diff --git a/LICENCE b/LICENCE
new file mode 100644
index 0000000..0f63eb9
--- /dev/null
+++ b/LICENCE
@@ -0,0 +1,8 @@
+The MIT License (MIT)
+Copyright (c) 2017 Kanshi TANAIKE
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..df66509
--- /dev/null
+++ b/README.md
@@ -0,0 +1,1453 @@
+ggsrun
+=====
+
+
+# Table of Contents
+- [Overview](#Overview)
+- [Description](#Description)
+- [Demo](#Demo)
+- [Google APIs](#Google APIs)
+ - Comparison for Each Command
+ - Execution Flow of Commands ``exe1``, ``exe2`` and ``webapps``
+ - For High Security
+ - Installation Flow
+- [How to Install](#How to Install)
+ - [Install Execution API](#Install Execution API)
+ - [Install Web Apps](#Install Web Apps)
+- [How to Execute Google Apps Script Using ggsrun](#How to Execute Google Apps Script Using ggsrun)
+- [Samples](#Samples)
+ - Executes GAS and Retrieves Result Values
+ - Executes GAS with Values and Retrieves Feedbacked Values
+ - For Debug
+ - Executes GAS with Values and Downloads File
+ - Executes Existing Functions on Project
+ - Download Files
+ - Upload Files
+ - Show File List
+ - Search Files
+- [Applications](#Applications)
+ - For Sublime Text
+ - For CoffeeScript
+ - Create Triggers
+ - Link to Various Resources
+- [Appendix](#Appendix)
+ - Scopes
+ - Format of Data to GAS
+ - Access Log
+ - Experiment
+- [Q&A](#Q&A)
+- [Licence](#Licence)
+- [Author](#Author)
+- [Update History](#Update History)
+
+
+# Overview
+This is a CLI tool to execute Google Apps Script (GAS) on a terminal.
+
+
+# Description
+Will you want to develop GAS on your local PC? Generally, when we develop GAS, we have to login to Google using own browser and develop it on the Script Editor. Recently, I have wanted to have more convenient local-environment for developing GAS. So I created this "ggsrun". The main work is to execute GAS on local terminal and retrieve the results from Google.
+
+Features of "ggsrun" are as follows.
+
+1. **[Develops GAS using your terminal and text editor which got accustomed to using.](#demoterminal)**
+
+2. **[Executes GAS by giving values to your script.](#givevalues)**
+
+3. **[Executes GAS made of CoffeeScript.](#coffee)**
+
+4. **[Downloads spreadsheet, document and presentation, while executes GAS, simultaneously.](#filedownload)**
+
+5. **[Creates, updates and backs up project with GAS.](#fileupdate)**
+
+6. **[Downloads files from Google Drive and Uploads files to Google Drive.](#fileupdown)**
+
+You can see the release page [here](https://github.com/tanaikech/ggsrun/releases).
+
+
+# Demo
+This demonstration retrieves data from spreadsheet as shown in an image as an array. You can see the cell data retrieved by giving the range data.
+
+![](readme_sheet.png)
+
+The sample script is as follows.
+
+~~~javascript
+function work(range) {
+ var sheetid = '1zKYnvVAiwkzgWngAhaSGdKCf2EvFzwOpBgPRAZzsvtM';
+ var ss = SpreadsheetApp.openById(sheetid);
+ var sheet = ss.getSheetByName("sheet1");
+ var data = sheet.getRange(range).getValues();
+ return data;
+}
+
+function main(range) {
+ return work(range);
+}
+~~~
+
+![](readme_terminaldemo.gif)
+
+[Also you can see a demo for Sublime Text using ggsrun.](#demosublime)
+
+---
+
+
+# Google APIs
+ggsrun uses Execution API, Web Apps and Drive API on Google.
+
+1. **[Execution API](https://developers.google.com/apps-script/guides/rest/api)** : This can be used by the authorization process of OAuth2. So you can use under higher security. But there are some [limitations](https://developers.google.com/apps-script/guides/rest/api#limitations).
+
+2. **[Drive API](https://developers.google.com/drive/v3/web/about-sdk)** : This can be used for downloading and uploading files.
+
+3. **[Web Apps](https://developers.google.com/apps-script/guides/web)** : This can be used without the authorization process. So a password can be set for using the server. The install process is very simple. However, the security is much lower than that of Execution API. ggsrun has to setup the access to Web Apps as "Anyone, even anonymous". There are no limitations. (I haven't confirmed it yet.) (I don't know whether this can be called an API.)
+
+And also ggsrun uses a server script (ggsrunif) which is deployed at Google side as an interface for executing GAS script. **This server script is provided as a GAS library. Using APIs and the server, there are 4 methods for executing GAS script.** These methods are provided as 4 commands. The comparison for each command is as follows.
+
+## Comparison for Each Command
+Each command is used as ``$ ggsrun [Command] [Options]``.
+
+| | Command | Execution method | Security | Response speed | Access token | Server | Authorization
for Google Services | Script | Available scripts | Call function | Limitation | Error Message | Library\*6 |
+|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
+| 1 | exe1
(e1) | Execution API | High | Slow\*1 | Yes | No | No | Upload
& Save to project
| Only standalone script | Functions in project | [Some limitations](https://developers.google.com/apps-script/guides/rest/api) | Error message
Line number\*5 | Yes |
+| 2 | exe1
(e1)
``-f`` | Execution API | High | Fast | Yes | No | No | No upload
& Only execute function on project | Standalone and Container-bound Script | Functions in project | [Some limitations](https://developers.google.com/apps-script/guides/rest/api) | Error message
Line number\*5 | Yes |
+| 3 | exe2
(e2) | Execution API | High | Fast | Yes | Yes | No | Upload
& No save | Standalone and Container-bound Script | Functions in execution script | [Some limitations](https://developers.google.com/apps-script/guides/rest/api) | Error message\*5 | No |
+| 4 | webapps
(w) | Web Apps | Low\*2 | Fast | No\*2 | Yes | Yes\*3 | Upload
& No save | Standalone and Container-bound Script | Functions in execution script | \*4 | Error message\*5 | No |
+
+* \*1: At execution of GAS using "webapps", project is updated. So at first, project is downloaded and the project is backed up, your script adds to the project, the project is uploaded with existing project, and project is updated. Therefore, the speed becomes slow.
+* \*2: Execution of GAS using "webapps" is not required authorization.
+* \*3: [If the script includes API that the authorization is necessary, it has to be authorized for script on script editor.](https://developers.google.com/apps-script/guides/services/authorization)
+* \*4: I have never confirmed about the limitation for Web Apps yet.
+* \*5: For "exe1", when an error occurs, you can see the error message and line number at error. For "exe2" and "webapps", when an error occurs, you can see the error message. On the other hand, line number at error is NOT shown because the script is sent as one line.
+* \*6: The server script is installed as a library. Here, whether libraries except for the server can be used.
+
+Please select one from above 4 commands to use ggsrun. I think that ``exe2`` is high security, high speed, easy to use, and it has some specialized functions. So I always use command ``exe2``.
+
+
+## Execution Flow of Commands ``exe1``, ``exe2`` and ``webapps``
+Here, I explain the flow difference of ``exe1`` and ``exe2`` using flow of each command. The differences of functions have already been explained [above](#comparison).
+
+
+### Flow of ``exe1``
+![](readme_Flow_exe1.png)
+
+1. Prepare sample script of GAS. For example, it's ``sample.gs``.
+1. Execute ggsrun using command ``exe1``.
+1. [URL request] At first, the project, which was enabled Execution API, on Google Drive is downloaded for the back up.
+1. Add ``sample.gs`` to the downloaded project as a script with name of ``ggsrun.gs``.
+1. [URL request] The project with ``sample.gs`` is uploaded and overwrites the project on Google.
+1. [URL request] **Using Execution API, directly execute the function ``main()``** which is a default executing function and retrieves response. Of course, you can choose the function using option ``-f``.
+
+In this case, there are 3 times URL requests.
+
+When I executed above flow for the first time, I felt the low speed and high API cost. So in order to compare them, I made ``exe2`` and ``webapps``.
+
+### Flow of ``exe2`` and ``webapps``
+![](readme_Flow_exe2.png)
+
+The flow of ``exe2`` and ``webapps`` is the same.
+
+1. Prepare sample script of GAS. For example, it's ``sample.gs``.
+1. Execute ggsrun using command ``exe2`` or ``webapps``.
+1. [URL request] ``sample.gs`` is uploaded as a value, it executes the server function ``ggsrunif.ExecutionApi()`` for ``exe2`` or ``doPost()`` for ``webapps`` using Execution API.
+1. In the server function, **Using ``(0,eval)(value)``, the function ``main()`` of the uploaded script is executed.** And then, retrieves the response in this one request.
+
+In this case, there is only 1 URL request.
+
+**Please don't worry. When you saw the flow of ``exe2``, if you don't want to use the server, you can use ggsrun using only ``exe1``. ``exe1`` doesn't required the server. And if you don't use ``doPost()`` and don't deploy web apps, ``webapps`` cannot be used by yourself and others.**
+
+The time due to API costs of ``exe2`` is shorter than that of ``exe1``. When the process time of script is small, you will be able to feel the shorter time by the low API cost. But, when the execution time of GAS script is close to the limitation time (6 min), the time due to API costs might not feel.
+
+## For High Security
+Because ``(0,eval)(value)`` is used for the commands ``exe2`` and ``webapps``, I recommends ``exe1`` which has to authorize OAuth2. However, there are some functions which can be executed by only ``webapps``. Therefore, it requires user's careful-judgment.
+
+When you use ``exe1`` and ``exe2``, please don't public the client ID, client secret, refresh token and access token for the project with server. When you use ``webapps``, please don't public the URL of web application.
+
+
+## Installation Flow
+This is an installation flow for Execution API which is used for command ``exe1`` and ``exe2``, and Web Apps which is used command ``webapps``. Don't worry. These installations are not difficult. The details are [How to install Execution API](#installexecutionapi) and [How to install Web Apps](#installwebapps).
+
+![](readme_flow.png)
+
+If you want to trial test using very simple installation, please check [Trial Usage](#Trial Usage).
+
+
+
+# How to Install
+Two important scripts are necessary to use ggsrun.
+
+##### 1. Client script which is run on local PC
+
+##### 2. Server script which is run on Google
+
+## 1. Get ggsrun Client (at local PC side)
+Download an executable file of ggsrun from [the release page](https://github.com/tanaikech/ggsrun/releases) and import to a directory with path.
+
+or
+
+Use go get.
+
+~~~bash
+$ go get -u github.com/tanaikech/ggsrun
+~~~
+
+## 2. Setup ggsrun Server (at Google side)
+This is a common setup for Execution API and Web Apps.
+
+#### 1. Create project
+On Google Drive, a project can be created as a standalone script or a container-bound script. The project can have several script in it. Please create a project, and then open the Script Editor. If a standalone script is created, you can use all functions on ggsrun. If a container-bound script is created, you cannot use command ``exe1``.
+
+- Open the project. And please operate follows using click.
+- -> File
+- -> Project properties
+- -> **Get Script ID**
+
+#### 2. Install server
+**ggsrun server is provided as a GAS library.** This can be used for a standalone script and a container-bound script. [If you want to read about Libraries, please check this.](https://developers.google.com/apps-script/guide_libraries).
+
+- Open the project. And please operate follows by click.
+- -> Resource
+- -> Library
+- -> Input Script ID of "**``115-19njNHlbT-NI0hMPDnVO1sdrw2tJKCAJgOTIAPbi_jq3tOo4lVRov``**" to text box
+- -> Add library
+- -> Please select latest version
+- -> Developer mode ON (If you don't want to use latest version, please select others.)
+- -> Identifier is "**``ggsrunif``**". (This is a important point)
+
+
+## Install Execution API
+By installing this, you can use command ``exe1`` and ``exe2``. To use command ``exe1``, the project installed server has to be a standalone script. For the command ``exe2``, you can use a standalone script and a container-bound script.
+
+#### 1. [Deploy API executable](https://developers.google.com/apps-script/guides/rest/api)
+1. On the Script Editor
+ - -> Publish
+ - -> Deploy as API executable
+ - -> Choose latest version and me as access
+ - **Ignore Current API ID**
+
+
+#### 2. Enable APIs (Execution API and Drive API)
+1. On the Script Editor
+ - -> Resources
+ - -> Cloud Platform Project
+ - -> Click "Projects currently associated with this script"
+2. On the Console Project
+ - Library at left side
+ - -> Search "execution api", Click it
+ - -> **Enable "Google Apps Script Execution API"**
+ - Library at left side
+ - -> Search "drive api", Click it
+ - -> **Enable "Google Drive API"**
+
+#### 3. Get Client ID, Client Secret
+1. On the Console Project
+ - Authentication information at left side
+ - -> Create a valid Client ID as OAyth client ID
+ - -> Choose **etc**
+ - -> Input Name (This is a name you want.)
+ - -> done
+ - -> Download a JSON file with Client ID and Client Secret as **``client_secret.json``** using download button. Please rename the file name to **``client_secret.json``**.
+
+#### 4. Create configure file (**``ggsrun.cfg``**)
+1. Please run ggsrun with ``auth`` command on your terminal (and/or command prompt) as follows. At this time, ggsrun has to be run at the directory with ``client_secret.json``.
+
+```
+$ ggsrun auth
+```
+- When above is run, your browser is launched and waits for login to Google.
+- Please login to Google.
+- [Authorization for Google Services](https://developers.google.com/apps-script/guides/services/authorization) is launched. Please authorize it.
+- The authorization code can be retrieved automatically. And ``Done.`` is displayed on your terminal.
+ - If your browser isn't launched or spends for 30 seconds from the wait of authorization, it becomes the input work queue. This is a manual mode. Please copy displayed URL and paste it to your browser, and login to Google. A **code** is displayed on the browser. Please copy it and paste it to your terminal during the input work queue. If you cannot find the code on your browser, please check the URL of your browser.
+- When ``Done`` is displayed on your terminal, the authorization is completed and ``ggsrun.cfg`` is created on a directory you currently stay.
+- ``ggsrun.cfg`` includes client id, client secret, access token, refresh token, scopes and so on.
+- At the default, there are 6 scopes. If you want to change the scopes, please modify ``ggsrun.cfg`` and run ``$ ggsrun auth``.
+- You notice that ``script_id`` has no data. Don't worry. This is explained below.
+
+#### 5. Run ggsrun
+Please create ``sample.gs`` with following script.
+
+```
+function main(){return Beacon()}
+```
+1. **Execution function in the GAS file is ``main()``. This is fixed.** Although you can use other name's functions in the GAS file like [this sample](#coffee), the first execution function is ``main()``.
+2. Here, Script ID is required. The Script ID is Script ID of project installed server. You can get as follows.
+ - Open Script Editor of the script with server
+ - -> File
+ - -> Object property
+ - -> Script ID
+3. Execute ``sample.gs`` as follows. After ggsrun was run using option ``-i [Script ID]``, the script ID is saved in ``ggsrun.cfg``. So after this, the option ``-i`` for running ggsrun is NOT required. ``-j`` displays JSON result by parsing.
+
+```
+$ ggsrun e2 -s sample.gs -i [Script ID] -j
+```
+
+If following result is shown, installing ggsrun is finished. Enjoy developing GAS!
+
+~~~json
+{
+ "result": "This is a server for ggsrun. Version is 1.0.0. Autor is https://github.com/tanaikech .",
+ "GoogleElapsedTime": 0.001,
+ "TotalElapsedTime": 1.416,
+ "ScriptDate": "2017-01-01_00:00:00_GMT",
+ "API": "Execution API with server",
+ "message": [
+ "'main()' in the script was run using ggsrun server. Server function is 'ggsrunif.ExecutionApi()'."
+ ]
+}
+~~~
+
+### If an Error Occurred at First Run
+If a following error occurs,
+
+```
+$ ./ggsrun.exe e2 -s sample.gs
+Error: Status Code: 404.
+{
+ "error": {
+ "code": 404,
+ "message": "Requested entity was not found.",
+ "status": "NOT_FOUND"
+ }
+}
+```
+please confirm following settings.
+
+- Whether Execution API at [Google API Console](https://console.developers.google.com/) is enable.
+- Whether for the project installed ggsrun server, API executable has been deployed as latest version.
+- **After deployed API executable, each script in the project has been saved by the save button. This is important point!**
+
+### About Timeout of Execution
+Now the script runtime is 6 min/execution at [Google](https://developers.google.com/apps-script/guides/services/quotas). By this, the current timeout of request for sending GAS script is 370 seconds. Alto this condition is the same to Web Apps.
+
+### Authorization for Google Services
+When you use GAS, you have ever seen [a message of "Authorization required"](https://developers.google.com/apps-script/guides/services/authorization). When you execute GAS using ggsrun, such authorization message is displayed as an error message.
+
+**If you want to use spreadsheet and other Google APIs in Google Apps Script, please do authorization for each API on GAS script editor, before run ggsrun. When the authorization error occurs by running ggsrun client, check your GAS script and do authorization of API.**
+
+### Help
+#### Command ``exe1``
+~~~
+$ ggsrun e1 -help
+NAME:
+ ggsrun.exe exe1 - Updates project and Executes the function in the project.
+
+USAGE:
+ ggsrun.exe exe1 [command options] [arguments...]
+
+DESCRIPTION:
+ In this mode, an access token is required.
+
+OPTIONS:
+ --scriptid value, -i value Script ID of project on Google Drive
+ --scriptfile value, -s value GAS file (.gs, .gas, .js, .coffee) on local PC
+ --function value, -f value Function name which is executed. Default is 'main'.
+ --value value, -v value Give a value to the function which is executed.
+ --backup, -b Backup project with script ID you set as a file.
+ --onlyresult, -r Display only 'result' in JSON results
+ --jsonparser, -j Display results by JSON parser
+~~~
+#### Command ``exe2``
+~~~
+$ ggsrun e2 -help
+NAME:
+ ggsrun.exe exe2 - Uploads GAS and Executes the script using Execution API.
+
+USAGE:
+ ggsrun.exe exe2 [command options] [arguments...]
+
+DESCRIPTION:
+ In this mode, an access token is required.
+
+OPTIONS:
+ --scriptid value, -i value Script ID of project on Google Drive
+ --scriptfile value, -s value GAS file (.gs, .gas, .js, .coffee) on local PC
+ --function value, -f value Function name of server for executing GAS. Default is 'ggsrunif.ExecutionApi'. If you change the server, use this.
+ --value value, -v value Give a value to the function of GAS script which is executed.
+ --stringscript value, --ss value GAS script as strings.
+ --foldertree, -t Display a folder tree on Google Drive as an array.
+ --convert, --conv [Experiment] Download file using byte slice data. Use with '-v [File ID]'.
+ --log, -l Record access log.
+ --onlyresult, -r Display only 'result' in JSON results
+ --jsonparser, -j Display results by JSON parser
+~~~
+
+---
+
+
+## Install Web Apps
+By installing this, you can use command ``webapps``.
+
+For the command ``webapps``, the authorization is NOT required. Web Apps is used for it. You may have already use it as ``doGet(e)`` and ``doPost(e)``. I applied this to ggsrun.
+
+**The project which was installed ggsrun server can be used both Execution API and Web Apps. So following flow can be accommodated to the project created at above section.**
+
+#### 1. [Deploy Web Apps](https://developers.google.com/apps-script/guides/web)
+* Copy the following script and paste it to the project with server on Script Editor. This ``doPost()`` is executed. ``ggsrunif`` is the library of server.
+
+```
+function doPost(e) {return ggsrunif.WebApps(e, "password")}
+```
+* ``password`` is used for using Web Apps at the server. You can select the password freely. In order to improve security even a little, it was introduced.
+* On the Script Editor
+ - File
+ - -> Manage Versions
+ - -> Save New Version
+ - Publish
+ - -> Deploy as Web App
+ - -> At Execute the app as, select **"your account"**
+ - -> At Who has access to the app, select **"Anyone, even anonymous"**
+ - -> Click "Deploy"
+ - -> Copy **"Current web app URL"**
+ - -> Click "OK"
+
+#### 2. Run ggsrun
+Please create ``sample.gs`` with following script.
+
+```
+function main(){return Beacon()}
+```
+1. **Execution function in the GAS file is ``main()``. This is fixed.** Although you can use other name's functions in the GAS file like [this sample](#coffee), the first execution function is ``main()``.
+2. Confirm URL of Web Apps. URL is ``https://script.google.com/macros/s/#####/exec``.
+3. Execute ``sample.gs``.
+
+```
+$ ggsrun w -s sample.gs -p password -u https://script.google.com/macros/s/#####/exec -j
+```
+If following result is shown when you run above, install of ggsrun for Web Apps is finished. Enjoy developing GAS!
+
+~~~json
+{
+ "result": "This is a server for ggsrun. Version is 1.0.0. Autor is https://github.com/tanaikech .",
+ "GoogleElapsedTime": 0.15,
+ "TotalElapsedTime": 1.945,
+ "ScriptDate": "2017-01-01_00:00:00_GMT",
+ "API": "Web Apps with server"
+}
+~~~
+
+### Help
+~~~
+$ ggsrun w --help
+NAME:
+ ggsrun.exe webapps - Uploads GAS and Executes the script without OAuth using Web Apps.
+
+USAGE:
+ ggsrun.exe webapps [command options] [arguments...]
+
+DESCRIPTION:
+ In this mode, an access token is NOT required.
+
+OPTIONS:
+ --url value, -u value URL for using Web Apps.
+ --scriptfile value, -s value GAS file (.gs, .gas, .js, .coffee) on local PC
+ --value value, -v value Give a value to the function of GAS script which is executed.
+ --password value, -p value Password to use Web Apps (if you have set)
+ --log, -l Not record access log. No this option means 'Record log'.
+ --onlyresult, -r Display only 'result' in JSON results
+ --jsonparser, -j Display results by JSON parser
+~~~
+
+---
+
+
+# How to Execute Google Apps Script Using ggsrun
+When you have the configure file ``ggsrun.cfg``, you can execute GAS. If you cannot find it, please download ``client_secret.json`` and run
+
+```
+$ ggsrun auth
+```
+
+By this, ``ggsrun.cfg`` is created on your working directory.
+
+In the case of using Execution API,
+
+~~~bash
+$ ggsrun e1 -s sample.gs
+~~~
+
+If you want to execute CoffeeScript, although you can do it changing from ``sample.gs`` to ``sample.coffee``, please read the section of [CoffeeScript](#CoffeeScript). If you want to execute a function except for ``main()`` of default, you can use an option like ``-f foo``. This command ``exe1`` can be used to execute a function on project.
+
+~~~bash
+$ ggsrun e1 -f foo
+~~~
+
+~~~bash
+$ ggsrun e2 -s sample.gs
+~~~
+
+At ``e2``, you cannot select the executing function except for ``main()`` of default.
+
+``e1``, ``e2`` and ``-s`` mean using Execution API and GAS script file name, respectively. Sample codes which are shown here will be used Execution API. At this time, the executing function is ``main()``, which is a default, in the script.
+
+In the case of using Web Apps,
+
+~~~bash
+$ ggsrun w -s sample.gs -p password -u [ WebApps URL ]
+~~~
+
+``w`` and ``-p`` mean using Web Apps and password you set at the server side, respectively. Using ``-u`` it imports Web Apps URL like ``-u https://script.google.com/macros/s/#####/exec``.
+
+----
+
+
+# Samples
+Here, it introduces some samples for using ggsrun. Following samples are supposed that command ``exe2`` is used. If you want to use ``exe1`` and ``webbapps``, please change from ``e2`` to ``e1`` and ``w``. Basically usage is the same for all.
+
+## 1. Executes GAS and Retrieves Result Values
+This may be most popular usage for this tool.
+
+**You can retrieve the results of script using ``return`` in function ``main()``.** Although you can include various name's functions in your script, the function which firstly run is ``main()``.
+
+**Script :** sample.gs
+
+~~~javascript
+function main() {
+ return 1 + 1;
+}
+~~~
+
+**Run :**
+
+~~~bash
+$ ggsrun e2 -s sample.gs -j
+~~~
+
+**Execution function in the GAS file is ``main()``. This is fixed.** Although you can use other name's functions in the GAS file, the first execution function is ``main()``.
+
+Following results are output. The result is output as JSON.
+
+~~~json
+{
+ "result": 2,
+ "GoogleElapsedTime": 0.001,
+ "TotalElapsedTime": 1.309,
+ "ScriptDate": "2017-01-01_00:00:00_GMT",
+ "API": "Execution API with server",
+ "message": [
+ "'main()' in the script was run using ggsrun server. Server function is 'ggsrunif.ExecutionApi()'."
+ ]
+}
+~~~
+
+ - ``"result"`` is the result returned from ``main()`` function.
+ - ``"GoogleElapsedTime"`` is the elapsed time on Google. The unit is second.
+ - ``"TotalElapsedTime"`` is the total elapsed time. The unit is second.
+ - ``"ScriptDate"`` is the executed time of script. This is also used for a log saving on Google, if option ``-l`` is used.
+ - ``"API"`` is API you used.
+ - ``"message"`` is included executed function on Google, retrieving access token and error messages.
+
+If you want to get only result, you can run using option ``-r``.
+
+~~~bash
+$ ggsrun e2 -r -s sample.gs
+
+>>> 2
+~~~
+
+## 2. Executes GAS with Values and Retrieves Feedbacked Values
+### 1. Gives a number and executes GAS
+At the current version, the number of values which can give to function is only 1. So if you want to give several values to function, please use array and JSON.
+
+**Script :** sample.gs
+
+~~~javascript
+function main(values) {
+ return values + 1;
+}
+~~~
+
+**Run :**
+
+~~~bash
+$ ggsrun e2 -r -s sample.gs -v 1
+~~~
+
+**Result :**
+
+~~~
+2
+~~~
+
+### 2. Gives a String and Executes GAS
+If you want to use double quotations and single quotations, they have to be escaped like ``\"`` and ``\'``.
+
+**Script :** sample.gs
+
+~~~javascript
+function main(values) {
+ return values + "fuga";
+}
+~~~
+
+**Run : Pattern 1**
+
+~~~bash
+$ ggsrun e2 -r -s sample.gs -v hoge
+~~~
+
+**Run : Pattern 2**
+
+~~~bash
+$ ggsrun e2 -r -s sample.gs -v "hoge"
+~~~
+
+**Run : Pattern 3**
+
+~~~bash
+$ ggsrun e2 -r -s sample.gs -v 'hoge'
+~~~
+
+**Result :**
+
+~~~
+"hogefuga"
+~~~
+
+### 3. Gives an Array and Executes GAS
+Using an array, you can give some strings and numbers. **For array, space cannot be used. And single and double quotations have to be escaped. When you send a script and data, if an error occurred, please check the strings which should be escaped. It may lead to the solution of errors.**
+
+**Script :** sample.gs
+
+~~~javascript
+function main(ar) {
+ ar.push("fuga");
+ return ar;
+}
+~~~
+
+**Run :**
+
+~~~bash
+$ ggsrun e2 -r -s sample.gs -v [123,\"hoge\"]
+~~~
+
+**Result :**
+
+~~~json
+[123,"hoge","fuga"]
+~~~
+
+When you use option ``-j``, the result is parsed with indent like below.
+
+~~~bash
+$ ggsrun e2 -j -r -s sample.gs -v [123,\"hoge\"]
+~~~
+
+~~~json
+[
+ 123,
+ "hoge",
+ "fuga"
+]
+~~~
+
+### 4. Gives JSON and Executes GAS
+Using an array, you can give some strings and numbers. For JSON, also spaces cannot be used.
+
+**Script :** sample.gs
+
+~~~javascript
+function main(jdata) {
+ jdata.fuga = "hoge";
+ return jdata;
+}
+~~~
+
+**Run :**
+
+~~~bash
+$ ggsrun e2 -j -r -s sample.gs -v {\"hoge\":\"fuga\"\,\"foo\":123}
+~~~
+
+In this case, spaces cannot be used, and a comma has to be escaped like ``\,``. If you want to use some spaces and not to use ``\,``,
+
+~~~bash
+$ ggsrun e2 -j -r -s sample.gs -v "{\"hoge\":\"fuga\", \"foo\":123}"
+~~~
+
+Please use ``\"`` for the JSON keys.
+
+**Result :**
+
+~~~json
+{
+ "foo": 123,
+ "fuga": "hoge",
+ "hoge": "fuga"
+}
+~~~
+
+## 3. For Debug
+You know ``Logger.log()`` can be used for a simple debug in GAS. In the of ggsrun, when ``Logger.log()`` is used in a script, the result of it cannot be seen, because the log cannot be output by API. So I prepared a function instead of ``Logger.log()`` for ggsrun. That is ``Log()``. You can use it in your GAS script for using ggsrun. The sample script is as follows.
+
+**Script :** sample.gs
+
+~~~javascript
+function main() {
+ var n = 10;
+ var f1 = 1;
+ var f2 = 0;
+ var result = [[1]];
+ for(var i = 0; i < n - 1; i++) {
+ var temp = f1;
+ f1 += f2;
+ f2 = temp;
+
+ if (f1 % 3 == 0) {
+ Log(f1); // Here, output log
+ }
+
+ result[i + 1] = [f1];
+ }
+ return result;
+}
+~~~
+
+**Run :**
+
+~~~bash
+$ ggsrun e2 -s sample.gs
+~~~
+
+~~~json
+{
+ "result":[[1],[1],[2],[3],[5],[8],[13],[21],[34],[55]],
+ "logger":[3,21],
+ "GoogleElapsedTime":0.002,
+ "TotalElapsedTime":1.397,
+ "ScriptDate":"2017-04-10_01:27:51_GMT",
+ "API":"Execution API with server",
+ "message":["'main()' in the script was run using ggsrun server. Server function is 'ggsrunif.ExecutionApi()'."]
+}
+~~~
+
+You can retrieve logs as ``logger``. If you use option ``-r``, only result is displayed and log is NOT displayed. You can use this for the simple debug.
+
+For "exe1", when an error occurs, you can see the error message and line number at error. For "exe2" and "webapps", when an error occurs, you can see the error message. On the other hand, line number at error is NOT shown because the script is sent as one line.
+
+When a result with error of "exe1" is below. You can see various error messages.
+
+~~~json
+{
+ "result": ###,
+ "TotalElapsedTime": 1.0,
+ "API": "Execution API without server",
+ "message": [
+ "{code: #, message: ReferenceError, function: test, linenumber: #}",
+ "{detailmessage: ## is not defined.}",
+ "Function 'test()' was run."
+ ]
+}
+~~~
+
+**So I always create GAS script using the command "exe2". When an error occurred, I debug the script using the command "exe1" and the method ``Log()``. By this, the developing efficiency becomes high.**
+
+## 4. Executes GAS with Values and Downloads File
+At command ``exe2``, you can execute script and download file, simultaneously. So you can download file using ID created in the script. This cannot be used for ``exe1`` and ``webapps``.
+
+**Script :** sample.gs
+
+~~~javascript
+function main(values) {
+ var r = values + 1;
+ return {res: r, fileid: '###', extension: 'pdf'};
+}
+~~~
+
+**Run :**
+
+~~~bash
+$ ggsrun e2 -s sample.gs -v 1 -j
+~~~
+
+**Result :**
+
+~~~json
+{
+ "result": {
+ "res": 2
+ },
+ "GoogleElapsedTime": 0.001,
+ "TotalElapsedTime": 1.31,
+ "ScriptDate": "2017-01-01_00:00:00_GMT",
+ "API": "Execution API with server",
+ "message": [
+ "File was downloaded as 'filename.pdf'.",
+ "'main()' in the script was run using ggsrun server. Server function is 'ggsrunif.ExecutionApi()'."
+ ]
+}
+~~~
+
+By using ``{res: r, fileid: '###', extension: 'pdf'}`` as a return, a file can be downloaded, simultaneously. For ``res``, you can choose freely. But both ``fileid`` and ``extension`` are the reserved words.
+
+## 5. Executes Existing Functions on Project
+Of course, ggsrun can execute a function on project. In this case, ``exe1`` is used. Also you can use ``-v`` for the function.
+
+**Function on project :**
+
+It supposes that this function is already existing on the project with server.
+
+~~~javascript
+function foo(values) {
+ return values + 1;
+}
+~~~
+
+**Run :**
+
+~~~bash
+$ ggsrun e1 -f foo -v 1 -j
+~~~
+
+**Result :**
+
+~~~json
+{
+ "result": 2,
+ "TotalElapsedTime": 1.214,
+ "API": "Execution API without server",
+ "message": [
+ "Function 'foo()' was run."
+ ]
+}
+~~~
+
+``GoogleElapsedTime`` cannot output in this mode, because this mode doesn't use server.
+
+## 6. Download Files
+ggsrun can download files from Google Drive by file ID and file name. The files also include GAS projects and scripts.
+
+**Run :**
+
+~~~bash
+$ ggsrun d -f filename
+~~~
+
+~~~bash
+$ ggsrun d -i fileid
+~~~
+
+If you want to convert and download files, you can use an option ``-e [extension]``. In the case of conversion from Google Docs to pdf file,
+
+~~~bash
+$ ggsrun d -f filename -e pdf
+~~~
+
+You can convert only from Google Docs Files (spreadsheet, slide, documentation and so on). For example, you cannot convert image files and text data.
+
+Here, I could notice that the container-bound scripts can be downloaded!
+
+### Help
+~~~
+$ ggsrun d -h
+NAME:
+ ggsrun.exe download - Downloads files from Google Drive.
+
+USAGE:
+ ggsrun.exe download [command options] [arguments...]
+
+DESCRIPTION:
+ In this mode, an access token is required.
+
+OPTIONS:
+ --fileid value, -i value File ID on Google Drive
+ --filename value, -f value File Name on Google Drive
+ --extension value, -e value Extension (File format of downloaded file)
+ --rawdata, -r Save a project with GAS scripts as raw data (JSON data).
+ --jsonparser, -j Display results by JSON parser
+~~~
+
+## 7. Upload Files
+ggsrun can upload local files to Google Drive. The files also include GAS projects and scripts.
+
+**Run :** Uploads files except for scripts
+
+~~~bash
+$ ggsrun u -f filename
+~~~
+
+At this time, you can upload files to the specific folder using option ``-p [parent folder ID]``. When Microsoft Word, Excel and Powerpoint files are uploaded, they are automatically converted to Google Docs. If you don't want to convert them, please use option ``--nc``.
+
+**Run :** Uploads scripts
+
+This is not the update. This is uploaded a new file to Google Drive.
+
+~~~bash
+$ ggsrun u -f [script filename .gs, .gas, .js]
+~~~
+
+Files upload and convert to GAS. If you want to create a project using several scripts and HTMLs, please use as follows.
+
+~~~bash
+$ ggsrun u --pn [project name] -f script1.gs,script2.gs,index.html
+~~~
+
+These files are uploaded and converted to a project with the project name. When you open the created project, you can see these files in the project. The order of files is the same to the order when was uploaded.
+
+### Help
+~~~
+$ ggsrun u -h
+NAME:
+ ggsrun.exe upload - Uploads files to Google Drive.
+
+USAGE:
+ ggsrun.exe upload [command options] [arguments...]
+
+DESCRIPTION:
+ In this mode, an access token is required.
+
+OPTIONS:
+ --filename value, -f value File Name on local PC
+ --parentfolderid value, -p value Folder ID of parent folder on Google Drive
+ --projectname value, --pn value Upload several GAS scripts as a project.
+ --noconvert, --nc If you don't want to convert file to Google Apps format.
+ --jsonparser, -j Display results by JSON parser
+~~~
+
+## 8. Show File List
+ggsrun can retrieve file list on Google Drive. The list also includes folders.
+
+**Run :**
+
+~~~bash
+$ ggsrun ls -j
+~~~
+
+**Result :**
+
+In the case of ``ggsrun ls -j``, a statistical table is retrieved. But in the message, it shows ``If you want a file list, please use option '-s' or '-f'. The file name is automatically given.``. Using ``ggsrun ls -s -j``, file list is displayed to standard output. Using ``ggsrun ls -s -f``, file list is output to a file.
+
+~~~json
+{
+ "message": [
+ "Total: 20, File: 15, Folder: 5",
+ "If you want a file list, please use option '-s' or '-f'. The file name is automatically given."
+ ]
+}
+~~~
+
+### Help
+
+~~~
+$ ggsrun ls -h
+NAME:
+ ggsrun.exe filelist - Outputs a file list on Google Drive.
+
+USAGE:
+ ggsrun.exe filelist [command options] [arguments...]
+
+DESCRIPTION:
+ In this mode, an access token is required.
+
+OPTIONS:
+ --searchbyname value, --sn value Search file using File Name. Output File ID.
+ --searchbyid value, --si value Search file using File ID. Output File Name.
+ --stdout, -s Output all file list to standard output.
+ --file, -f Output all file list to a JSON file.
+ --jsonparser, -j Display results by JSON parser
+~~~
+
+
+## 9. Search Files
+ggsrun can search files on Google Drive.
+
+**Run :**
+
+~~~bash
+$ ggsrun ls -sn [file name] -j
+~~~
+
+~~~bash
+$ ggsrun ls -si [file id] -j
+~~~
+
+**Result :**
+
+~~~json
+{
+ Name: "[file name]",
+ ID: "[file id]",
+ ModifiedTime: "2017-01-01T00:00:00.000Z",
+ URL: "https://docs.google.com/spreadsheets/d/#####/edit?usp=drivesdk"
+}
+~~~
+
+Result includes file name, file id, modified time and URL.
+
+---
+
+# Applications
+
+## 1. For Sublime Text
+ggsrun can be also used as a builder for Sublime Text. Recently I had wished if GAS written by CoffeeScript could be built on Sublime Text. This is my large motivation for creating ggsrun. Now this was achieved. :)
+
+![](readme_sublimedemo.gif)
+
+Sample script is CoffeeScript as follows. The spreadsheet is the same to that of [terminal demo](#demoterminal). The terminal demo is not CoffeeScript.
+
+#### Script :
+
+~~~coffeescript
+work = (range) ->
+ sheetid = '1zKYnvVAiwkzgWngAhaSGdKCf2EvFzwOpBgPRAZzsvtM'
+ ss = SpreadsheetApp.openById(sheetid)
+ sheet = ss.getSheetByName("sheet1")
+ sheet.getRange(range).getValues()
+
+main = () ->
+ range = 'a1:c5'
+ work(range)
+~~~
+
+``GAS.sublime-build`` is below. At this time, the current working directory is the same to source file. Please put ``ggsrun.cfg`` in the same directory with the source files. And in the ``GAS.sublime-build``, the path for ggsrun may be needed. Also ggsrun may be able to be used for other text editors. Please enjoy developing GAS using the editor you love!
+
+If you could confirm that ggsrun can be used on other editor, I'm glad if you tell me.
+
+#### ``GAS.sublime-build`` :
+Since the file format is evaluated by the extension, GAS and CoffeeScript can be built by only this ``GAS.sublime-build``.
+
+~~~json
+{
+ "cmd": [
+ "ggsrun",
+ "exe2",
+ "--onlyresult",
+ "--scriptfile=${file}"
+ ],
+ "file_regex": "^(..[^:]*):([0-9]+):?([0-9]+)?:? (.*)$",
+ "selector": "source.gs, source.gas, source.coffee",
+ "shell":"true",
+ "encoding": "UTF-8"
+}
+~~~
+
+
+## 2. For CoffeeScript
+ggsrun can execute CoffeeScript. CoffeeScript can be still used for developing GAS. I prefer to use CoffeeSctipt. This is also one of my aims that I made ggsrun.
+
+**At the current stage, since GAS doesn't support ES2015 (ES6), please use CoffeeScript with version 1 series. CoffeeScript which used for this test is version 1.12.5. File format is evaluated by the extension.**
+
+In order to execute CoffeeScript, it required that CoffeeScript has already been installed. If you want to install CoffeeScript, please check here [http://coffeescript.org](http://coffeescript.org/).
+
+A flow of this sample script is as follows.
+
+ 1. Calculates Fibonacci numbers by giving ``para = 20``
+ 2. Creates new spreadsheet
+ 3. Imports the data to the spreadsheet
+ 4. Reads the data from the spreadsheet
+ 5. Downloads the spreadsheet as a CSV file
+
+### Sample script
+``sample.coffee``
+
+~~~coffeescript
+Fibonacci = (n) ->
+ f1 = 1
+ f2 = 0
+ r = [1]
+ for i in [0..n - 2]
+ t = f1
+ f1 += f2
+ f2 = t
+ r[i + 1] = f1
+ return r
+
+main = (para) -> # First execution function is main.
+ ar = Fibonacci(para)
+ ar = ([parseInt(i) + 1, e] for i, e of ar)
+ ss = SpreadsheetApp.create('samplesheet')
+ range = ss.getSheets()[0].getRange(1, 1, ar.length, ar[0].length)
+ range.setValues(ar)
+ data = range.getValues()
+ return res: data, fileid: ss.getId(), extension: "csv"
+~~~
+
+### Run
+
+~~~bash
+$ ggsrun e2 -s sample.coffee -v 20 -j
+~~~
+
+The compile option for CoffeeScript is ``coffee -cb``.
+
+### Result
+
+When above script is executed, the created csv file is also downloaded, simultaneously.
+
+~~~json
+{
+ "result": {
+ "res":[[1,1],[2,1],[3,2],[4,3],[5,5],[6,8],[7,13],[8,21],[9,34],[10,55],[11,89],[12,144],[13,233],[14,377],[15,610],[16,987],[17,1597],[18,2584],[19,4181],[20,6765]]
+ },
+ "GoogleElapsedTime": 1.44,
+ "TotalElapsedTime": 3.234,
+ "ScriptDate": "2017-01-01_00:00:00_GMT",
+ "API": "Execution API with server",
+ "message": [
+ "File was downloaded as 'samplesheet.csv'.",
+ "'main()' in the script was run using ggsrun server. Server function is 'ggsrunif.ExecutionApi()'."
+ ]
+}
+~~~
+
+**Downloaded CSV file :** ``samplesheet.csv``
+
+```
+1,1
+2,1
+3,2
+4,3
+5,5
+6,8
+7,13
+8,21
+9,34
+10,55
+11,89
+12,144
+13,233
+14,377
+15,610
+16,987
+17,1597
+18,2584
+19,4181
+20,6765
+```
+
+**And also I have created a GAS library made of CoffeeScript using ggsrun. You can see it [here](https://github.com/tanaikech/CreateImg).**
+
+## 3. Create Triggers
+In this sample, it creates a trigger using ggsrun on the project. Execution API cannot create triggers because of [the limitation](https://developers.google.com/apps-script/guides/rest/api). So it uses Web Apps of the command ``webapps`` to create the trigger. In this case, it supposes the deployment of Web Apps.
+
+### Script 1
+At first, please create following ``trigger.gs`` file. ``sheetid`` and ``sheetname`` are spreadsheet ID and sheet name, respectively. This function is run by the trigger. When this function run, "a1" of spreadsheet changes from "ggsrun" to "trigger".
+
+``trigger.gs``
+
+~~~javascript
+function runByTrigger(){
+ var sheetid = "#####";
+ var sheetname = "#####";
+ var ss = SpreadsheetApp.openById(sheetid).getSheetByName(sheetname);
+ ss.getRange("a1").setValue("trigger");
+}
+~~~
+
+### Script 2
+Next, please makes following ``create_trigger.gs`` file. ``time_min`` is imported when it is executed.
+
+``create_trigger.gs``
+
+~~~javascript
+function createTrigger(n, f){
+ if (ScriptApp.getProjectTriggers().length === 1){
+ ScriptApp.deleteTrigger(ScriptApp.getProjectTriggers()[0]);
+ }
+ var onTrigger = ScriptApp.newTrigger(f)
+ .timeBased()
+ .everyMinutes(parseInt(n))
+ .create();
+ return onTrigger.getHandlerFunction();
+}
+
+function main(time_min){
+ var sheetid = "#####";
+ var sheetname = "#####";
+ var ss = SpreadsheetApp.openById(sheetid).getSheetByName(sheetname);
+ ss.getRange("a1").setValue("ggsrun");
+ return createTrigger(time_min, "runByTrigger");
+}
+~~~
+
+### Run
+Executes 2 scripts in continuous. You can also use a shell script for this.
+
+~~~bash
+$ ggsrun e1 -s trigger.gs -f runByTrigger -j
+{
+ "result": null,
+ "TotalElapsedTime": 4.035,
+ "API": "Execution API without server",
+ "message": [
+ "Project was updated.",
+ "Function 'runByTrigger()' was run."
+ ]
+}
+~~~
+
+Using option ``-f``, it confirms errors in the sending script. No errors can be confirmed.
+
+~~~bash
+$ ggsrun w -s create_trigger.gs -v 5 -p password -u https://script.google.com/macros/s/#####/exec -j
+{
+ "result": "runByTrigger",
+ "GoogleElapsedTime": 0.822,
+ "TotalElapsedTime": 2.583,
+ "ScriptDate": "2017-01-01_00:00:00_GMT",
+ "API": "Web Apps with server"
+}
+~~~
+
+It returns the function name of ``runByTrigger`` without errors. When it opens the trigger list, the created trigger can be seen. And it was confirmed that "a1" of spreadsheet changed from "ggsrun" to "trigger". If you want to delete the trigger, please make the script and run ggsrun with the command ``webapps``.
+
+By this, it was found that the send of functions and the creation of new trigger are possible.
+
+## 4. Link to Various Resources
+This is also one of my aims that I made ggsrun. As a sample, it introduces a link to Python. For executing GAS, ggsrun can use not only files, but also string scripts. This is a sample for it.
+
+### Flow
+1. Set an initial variable ``n`` at Python script. ``n = 10''. This is sent to Google with the string script (``s1``).
+2. Do double ``n`` at Google. This is back to Python script.
+3. Do double the received value, create JSON data using Python script. This is sent to Google.
+4. Do double the received value at Google. This is back to Python script on local PC.
+
+### Script : Python
+
+~~~python
+import json
+import subprocess
+
+
+class ggsrun(object):
+
+ def ggsrun(self, values, stringscript):
+ return json.loads(self.__res_cmd(
+ "ggsrun" +
+ " exe2 --onlyresult --stringscript=\"" +
+ stringscript.replace("\"", "\\\"").replace("\n", "") +
+ "\" --value=" +
+ values
+ ).decode("utf8"))
+
+ def __res_cmd(self, cmd):
+ return subprocess.Popen(
+ cmd, stdout=subprocess.PIPE,
+ shell=True
+ ).communicate()[0]
+
+
+def main():
+ g = ggsrun()
+ s1 = """function main(dat){
+ return parseInt(dat) * 2;
+}"""
+ r1 = g.ggsrun(str(10), s1)
+ s2 = """function main(dat){
+ return {calculation: '""" + str(r1) + """ * 2 * 2' , result: parseInt(dat) * 2}
+}"""
+ r2 = g.ggsrun(str(r1 * 2), s2)
+ print(r1)
+ print(r2)
+
+
+if __name__ == '__main__':
+ main()
+~~~
+
+**The script which can be used for ``--stringscript`` is only GAS, not CoffeeScript. And also this cannot be included comment out using ``//``.**
+
+### Result
+
+~~~
+20
+{'calculation': '20 * 2 * 2', 'result': 80}
+~~~
+
+The GAS script can be modified by the received values from Google. I think that this indicates the possibility of the creations of various scripts with higher degrees of freedom using various languages.
+
+
+**I believe other applications. I'm very glad if you tell me when you found it.**
+
+---
+
+# Appendix
+## A1. Scopes
+As the default, 6 scopes are set as follows.
+
+- https://www.googleapis.com/auth/drive
+- https://www.googleapis.com/auth/drive.file
+- https://www.googleapis.com/auth/drive.scripts
+- https://www.googleapis.com/auth/script.external_request
+- https://www.googleapis.com/auth/script.scriptapp
+- https://www.googleapis.com/auth/spreadsheets
+
+If you want to change the scopes,
+
+1. Modify the scopes in ``ggsrun.cfg``.
+2. Run ``$ ggsrun auth``.
+
+When you run ggsrun , if you see an error message below,
+
+"**Authorization Error: Please check SCOPEs of your GAS script and server using GAS Script Editor.
+If the SCOPEs have changed, modify them in 'ggsrun.cfg' and delete a line of 'refresh_token', then, execute 'ggsrun' again. You can retrieve new access token with modified SCOPEs.**"
+
+Please confirm Scopes, which is used at your script, at the Script Editor on Google.
+
+## A2. Format of Data to GAS
+Here it describes about the format of data received at GAS on Google. Data you inputted is converted by ggsrun as follows.
+### For Execution Api
+```
+{
+ com: [strings and numbers sent by ggsrun] (string),
+ exefunc: Function name you want to execute (string),
+ Log: Whether it records a log. (boolean)
+}
+```
+This is a JSON format.
+
+### For Web Apps
+```
+e.parameters.pass // Password you set
+
+e.parameters.com // Strings and numbers sent by ggsrun
+
+e.parameters.log // Whether it records a log. (boolean)
+```
+
+You can see the detail of ``e.parameters`` at [https://developers.google.com/apps-script/guides/web](https://developers.google.com/apps-script/guides/web).
+
+## A3. Access Log
+ggsrun can record access log when a script is executed. ``exe1`` cannot record log, because it doesn't use server. ``exe2`` can record log using option ``-l``. When the option ``-l`` is NOT used, log is NOT recorded. ``webapps`` records log by compulsion, because ``webapps`` is NOT required the authorization.
+
+Access log which is a spreadsheet is ``ggsrun.log``. This is created besides the server project.
+
+## A4. Show Folder Tree
+ggsrun can retrieve a folder tree on Google Drive as an array. You can do it as follows.
+
+~~~bash
+$ ggsrun e2 -j -r -t
+~~~
+
+**Result :**
+
+~~~
+[
+ [foldername1(folder1 id)],
+ [foldername1(folder1 id), sub foldername1s(sub folde1s id)],
+ [foldername1(folder2 id)],
+ [foldername2(folder2 id), sub foldername2s1(sub folde2s1 id)],
+ [foldername2(folder2 id), sub foldername2s1(sub folde2s1 id), sub foldername2s2(sub folde2s2 id)]
+]
+~~~
+
+
+## A5. Experiment 1
+As an experiment, it added a function to download files without Drive API using only Execution API. Files on Google are converted to byte slice by the server and feedbacked it as a text data. It is reconstructed by ggsrun. This is just experiment. If you have some advices, feel free to tell me. At the current version, it leaves the mimeType to Google site.
+
+~~~bash
+$ ggsrun e2 --convert -j -v [file ID]
+~~~
+
+~~~json
+{
+ "result": "### Byte Slice of File ###",
+ "GoogleElapsedTime": 0.393,
+ "TotalElapsedTime": 2.343,
+ "ScriptDate": "2017-01-01_00:00:00_GMT",
+ "API": "Execution API with server",
+ "message": [
+ "'main()' in the script was run using ggsrun server. Server function is 'ggsrunif.ExecutionApi()'.",
+ "File was downloaded as 'samplesheet'. MimeType is 'application/pdf'."
+ ]
+}
+~~~
+
+
+## A6. Trial Usage
+Here, it introduces a Quickstart for ggsrun. So please also check [the detail information](#Google API).
+
+This is just very very simple trial test. I prepared this as a simple trial usage. You are not always necessary to do this trial.
+
+1. Download ggsrun [the release page](https://github.com/tanaikech/ggsrun/releases), or ``$ go get -u github.com/tanaikech/ggsrun``.
+2. Create a standalone Script on Google Drive, and install a server to the script as a library.
+ - Script ID is ``115-19njNHlbT-NI0hMPDnVO1sdrw2tJKCAJgOTIAPbi_jq3tOo4lVRov``.
+3. Copy and paste following function to the script.
+ - ``function doPost(e) {return ggsrunif.WebApps(e, "password")}``
+4. [Deploy Web Apps](https://developers.google.com/apps-script/guides/web) with
+ - File -> Manage Versions -> Save New Version - Publish -> Deploy as Web App -> At Execute the app as, select **"your account"** -> At Who has access to the app, select **"Anyone, even anonymous"** -> Click "Deploy" -> Copy **"Current web app URL"** -> Click "OK"
+5. Create GAS file as ``sample.gs``.
+ - ``function main(){return Beacon()}``
+6. Run ggsrun at terminal or command prompt. If the server information is displayed, it's done.
+ - ``$ ggsrun w -s sample.gs -p password -j -u [URL of web apps]``
+7. After this trial was done, **Please stop the deployed Web Apps.** Because the web apps is deployed for anyone.
+
+---
+
+
+# Q&A
+If your GAS script has errors when it executes at Google, it shows "Script Error on GAS side:". At that time, you can check your script. If it shows "Error:" without "GAS", that's the error inside ggsrun.
+
+When you executed a GAS script, if an error occurred, please check the strings which should be escaped. It may lead to the solution of errors.
+
+When an error occurs on ggsrun, solutions for most errors will be provided from ggsrun. However, also I think that there are errors I have never known. If you noticed bugs and errors of ggsrun, I'm glad when you tell me them. I would like to modify them. If the errors occur for your scripts when you use ggsrun, I think that modifying the errors will lead to the growth of this tool.
+
+#### 1. [Authorization for Google Services](https://developers.google.com/apps-script/guides/services/authorization) for your script
+- When you execute your GAS script, Authorization for Google Services may be required. At that time, only one time, please execute your script on Script Editor with the project installed the ggsrun server. And please auth Authorization for Google Services. After this, you can execute your script using ggsrun.
+
+#### 2. In the case that result is ``Script Error on GAS side: Insufficient Permission``
+- Please confirm [Authorization for Google Services](https://developers.google.com/apps-script/guides/services/authorization) for the script you use on Script Editor.
+- Please confirm about API you want to use at [Google API Console](https://console.developers.google.com).
+
+#### 3. In the case that a following error occurs
+```
+Error: Status Code: 404.
+{
+ "error": {
+ "code": 404,
+ "message": "Requested entity was not found.",
+ "status": "NOT_FOUND"
+ }
+}
+```
+Please confirm following settings.
+
+- Whether Execution API at [Google API Console](https://console.developers.google.com/) is enable.
+- Whether for the project installed ggsrun server, API executable has been deployed as latest version.
+- After deployed API executable, each script in the project has been saved. This is important point!
+
+#### 4. In the case that a following error occurs
+"**Script Error on GAS side: Script has attempted to perform an action that is not allowed when invoked through the Google Apps Script Execution API.**"
+
+When it uses ``exe1`` or ``exe2``, there is a case to see the error. That is one of [limitations](https://developers.google.com/apps-script/guides/rest/api#limitations) for ``exe1`` or ``exe2``. So please try to execute the same script using ``webapps``. It might be able to use.
+
+#### 5. In the case that a following error occurs
+Please be careful for ``;`` of end of each line of the script.
+When I have been creating sample GAS scripts for ggsrun, following error often occurred.
+
+```
+Missing ';' before statement.
+```
+
+In such case, please confirm whether ``;`` with end of each line in the script is placed to the correct position for Javascript. Although the script have no errors at Google Script Editor, when it is executed by ggsrun, the error occurred. Since ``exe2`` and ``webapps``of ggsrun send scripts as 1 line, it has to separate correctly between each line of the script. When it is executed by ``exe1``, no such errors occur since the function on Script Editor is directly called.
+
+When I had been developing applications using CoffeeScript and ggsrun, I have never seen above error yet.
+
+#### 6. Library
+You can use various libraries for GAS by ggsrun. But there are one limitation. When you want to use libraries, please add them to the project with server, and execute scripts using ``exe1`` mode. At ``exe2`` mode, additional library cannot be used, because the mode executes on the server script.
+
+
+
+# Licence
+[MIT](LICENCE)
+
+
+# Author
+[TANAIKE](https://github.com/tanaikech)
+
+If you have any questions and commissions for me, feel free to tell me using e-mail of tanaike@hotmail.com
+
+
+# Update History
+## ggsrun
+* v1.0.0 (April 24, 2017)
+
+ Initial release.
+
+## Server
+* v1.0.0 (April 24, 2017)
+
+ Initial release.
+
+[TOP](#TOP)
diff --git a/doc.go b/doc.go
new file mode 100644
index 0000000..4a80a70
--- /dev/null
+++ b/doc.go
@@ -0,0 +1,62 @@
+/*
+Package main (doc.go) :
+This is a CLI tool to execute Google Apps Script (GAS) on a terminal.
+
+Will you want to develop GAS on your local PC? Generally, when we develop GAS, we have to login to Google using own browser and develop it on the Script Editor. Recently, I have wanted to have more convenient local-environment for developing GAS. So I created this "ggsrun". The main work is to execute GAS on local terminal and retrieve the results from Google.
+
+# Features of "ggsrun" are as follows.
+
+1. Develops GAS using your terminal and text editor which got accustomed to using.
+
+2. Executes GAS by giving values to your script.
+
+3. Executes GAS made of CoffeeScript.
+
+4. Downloads spreadsheet, document and presentation, while executes GAS, simultaneously.
+
+5. Creates, updates and backs up project with GAS.
+
+6. Downloads files from Google Drive and Uploads files to Google Drive.
+
+You can see the release page https://github.com/tanaikech/ggsrun/releases
+
+# Google API
+
+ggsrun uses Execution API, Web Apps and Drive API on Google. About how to install ggsrun, please check my github repository.
+
+https://github.com/tanaikech/ggsrun/
+
+You can read the detail information there.
+
+
+---------------------------------------------------------------
+
+# How to Execute Google Apps Script Using ggsrun
+When you have the configure file `ggsrun.cfg`, you can execute GAS. If you cannot find it, please download `client_secret.json` and run
+
+$ ggsrun auth
+
+In the case of using Execution API,
+
+$ ggsrun e1 -s sample.gs
+
+If you want to execute a function except for `main()` of default, you can use an option like `-f foo`. This command `exe1` can be used to execute a function on project.
+
+$ ggsrun e1 -f foo
+
+$ ggsrun e2 -s sample.gs
+
+At `e2`, you cannot select the executing function except for `main()` of default.
+
+`e1`, `e2` and `-s` mean using Execution API and GAS script file name, respectively. Sample codes which are shown here will be used Execution API. At this time, the executing function is `main()`, which is a default, in the script.
+
+In the case of using Web Apps,
+
+$ ggsrun w -s sample.gs -p password -u [ WebApps URL ]
+
+`w` and `-p` mean using Web Apps and password you set at the server side, respectively. Using `-u` it imports Web Apps URL like `-u https://script.google.com/macros/s/#####/exec`.
+
+
+---------------------------------------------------------------
+*/
+package main
diff --git a/ggsrun.go b/ggsrun.go
new file mode 100644
index 0000000..2edd6b9
--- /dev/null
+++ b/ggsrun.go
@@ -0,0 +1,246 @@
+// Package main (ggsrun.go) :
+// This file is included all commands and options.
+package main
+
+import (
+ "os"
+
+ "github.com/urfave/cli"
+)
+
+// main : main function
+func main() {
+ app := cli.NewApp()
+ app.Name = appname
+ app.Author = "tanaike [ https://github.com/tanaikech/ggsrun ] "
+ app.Email = "tanaike@hotmail.com"
+ app.Usage = "Executes Google Apps Script (GAS) on Google and Feeds Back Results."
+ app.Version = "1.0.0"
+ app.Commands = []cli.Command{
+ {
+ Name: "exe1",
+ Aliases: []string{"e1"},
+ Usage: "Updates project and Executes the function in the project.",
+ Description: "In this mode, an access token is required.",
+ Action: exeAPIWithout,
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "scriptid, i",
+ Usage: "Script ID of project on Google Drive",
+ },
+ cli.StringFlag{
+ Name: "scriptfile, s",
+ Usage: "GAS file (.gs, .gas, .js, .coffee) on local PC",
+ },
+ cli.StringFlag{
+ Name: "function, f",
+ Usage: "Function name which is executed. Default is '" + deffuncwithout + "'.",
+ },
+ cli.StringFlag{
+ Name: "value, v",
+ Usage: "Give a value to the function which is executed.",
+ },
+ cli.BoolFlag{
+ Name: "backup, b",
+ Usage: "Backup project with script ID you set as a file.",
+ },
+ cli.BoolFlag{
+ Name: "onlyresult, r",
+ Usage: "Display only 'result' in JSON results",
+ },
+ cli.BoolFlag{
+ Name: "jsonparser, j",
+ Usage: "Display results by JSON parser",
+ },
+ },
+ },
+ {
+ Name: "exe2",
+ Aliases: []string{"e2"},
+ Usage: "Uploads GAS and Executes the script using Execution API.",
+ Description: "In this mode, an access token is required.",
+ Action: exeAPIWith,
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "scriptid, i",
+ Usage: "Script ID of project on Google Drive",
+ },
+ cli.StringFlag{
+ Name: "scriptfile, s",
+ Usage: "GAS file (.gs, .gas, .js, .coffee) on local PC",
+ },
+ cli.StringFlag{
+ Name: "function, f",
+ Usage: "Function name of server for executing GAS. Default is '" + deffuncserv + "'. If you change the server, use this.",
+ },
+ cli.StringFlag{
+ Name: "value, v",
+ Usage: "Give a value to the function of GAS script which is executed.",
+ },
+ cli.StringFlag{
+ Name: "stringscript, ss",
+ Usage: "GAS script as strings.",
+ },
+ cli.BoolFlag{
+ Name: "foldertree, t",
+ Usage: "Display a folder tree on Google Drive as an array.",
+ },
+ cli.BoolFlag{
+ Name: "convert, conv",
+ Usage: "[Experiment] Download file using byte slice data. Use with '-v [File ID]'.",
+ },
+ cli.BoolFlag{
+ Name: "log, l",
+ Usage: "Record access log.",
+ },
+ cli.BoolFlag{
+ Name: "onlyresult, r",
+ Usage: "Display only 'result' in JSON results",
+ },
+ cli.BoolFlag{
+ Name: "jsonparser, j",
+ Usage: "Display results by JSON parser",
+ },
+ },
+ },
+ {
+ Name: "webapps",
+ Aliases: []string{"w"},
+ Usage: "Uploads GAS and Executes the script without OAuth using Web Apps.",
+ Description: "In this mode, an access token is NOT required.",
+ Action: webAppsWith,
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "url, u",
+ Usage: "URL for using Web Apps.",
+ },
+ cli.StringFlag{
+ Name: "scriptfile, s",
+ Usage: "GAS file (.gs, .gas, .js, .coffee) on local PC",
+ },
+ cli.StringFlag{
+ Name: "value, v",
+ Usage: "Give a value to the function of GAS script which is executed.",
+ },
+ cli.StringFlag{
+ Name: "password, p",
+ Usage: "Password to use Web Apps (if you have set)",
+ },
+ cli.BoolFlag{
+ Name: "log, l",
+ Usage: "Not record access log. No this option means 'Record log'.",
+ },
+ cli.BoolFlag{
+ Name: "onlyresult, r",
+ Usage: "Display only 'result' in JSON results",
+ },
+ cli.BoolFlag{
+ Name: "jsonparser, j",
+ Usage: "Display results by JSON parser",
+ },
+ },
+ },
+ {
+ Name: "download",
+ Aliases: []string{"d"},
+ Usage: "Downloads files from Google Drive.",
+ Description: "In this mode, an access token is required.",
+ Action: downloadFiles,
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "fileid, i",
+ Usage: "File ID on Google Drive",
+ },
+ cli.StringFlag{
+ Name: "filename, f",
+ Usage: "File Name on Google Drive",
+ },
+ cli.StringFlag{
+ Name: "extension, e",
+ Usage: "Extension (File format of downloaded file)",
+ },
+ cli.BoolFlag{
+ Name: "rawdata, r",
+ Usage: "Save a project with GAS scripts as raw data (JSON data).",
+ },
+ cli.BoolFlag{
+ Name: "jsonparser, j",
+ Usage: "Display results by JSON parser",
+ },
+ },
+ },
+ {
+ Name: "upload",
+ Aliases: []string{"u"},
+ Usage: "Uploads files to Google Drive.",
+ Description: "In this mode, an access token is required.",
+ Action: uploadFiles,
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "filename, f",
+ Usage: "File Name on local PC",
+ },
+ cli.StringFlag{
+ Name: "parentfolderid, p",
+ Usage: "Folder ID of parent folder on Google Drive",
+ },
+ cli.StringFlag{
+ Name: "projectname, pn",
+ Usage: "Upload several GAS scripts as a project.",
+ },
+ cli.BoolFlag{
+ Name: "noconvert, nc",
+ Usage: "If you don't want to convert file to Google Apps format.",
+ },
+ cli.BoolFlag{
+ Name: "jsonparser, j",
+ Usage: "Display results by JSON parser",
+ },
+ },
+ },
+ {
+ Name: "filelist",
+ Aliases: []string{"ls"},
+ Usage: "Outputs a file list on Google Drive.",
+ Description: "In this mode, an access token is required.",
+ Action: showFileList,
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "searchbyname, sn",
+ Usage: "Search file using File Name. Output File ID.",
+ },
+ cli.StringFlag{
+ Name: "searchbyid, si",
+ Usage: "Search file using File ID. Output File Name.",
+ },
+ cli.BoolFlag{
+ Name: "stdout, s",
+ Usage: "Output all file list to standard output.",
+ },
+ cli.BoolFlag{
+ Name: "file, f",
+ Usage: "Output all file list to a JSON file.",
+ },
+ cli.BoolFlag{
+ Name: "jsonparser, j",
+ Usage: "Display results by JSON parser",
+ },
+ },
+ },
+ {
+ Name: "auth",
+ Usage: "Retrieve access and refresh tokens. If you changed scopes, please use this.",
+ Description: "In this mode, 'client_secret.json' and Scopes are required.",
+ Action: reAuth,
+ Flags: []cli.Flag{
+ cli.IntFlag{
+ Name: "port, p",
+ Usage: "Port number of temporal web server for retrieving authorization code.",
+ Value: 8080,
+ },
+ },
+ },
+ }
+ app.CommandNotFound = commandNotFound
+ app.Run(os.Args)
+}
diff --git a/handler.go b/handler.go
new file mode 100644
index 0000000..8907703
--- /dev/null
+++ b/handler.go
@@ -0,0 +1,131 @@
+// Package main (handler.go) :
+// Handler for ggsrun
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+
+ "github.com/tanaikech/ggsrun/utl"
+ "github.com/urfave/cli"
+)
+
+// exeAPIWithout : exe1
+// Update project and Execution API withour server script.
+func exeAPIWithout(c *cli.Context) error {
+ defAuthContainer(c).
+ ggsrunIni(c).
+ goauth().
+ defExecutionContainer().
+ exe1Function(c).
+ executionAPIwithoutServer(c).
+ esenderForExe1(c).
+ dispResult(c)
+ return nil
+}
+
+// exeAPIWith : exe2
+// No update project. Only execute GAS using Execution API with server script.
+func exeAPIWith(c *cli.Context) error {
+ defAuthContainer(c).
+ ggsrunIni(c).
+ goauth().
+ defExecutionContainer().
+ exe2Function(c).
+ dispResult(c)
+ return nil
+}
+
+// webAppsWith : exe3
+// No update project. Only execute GAS using Web Apps with server script.
+func webAppsWith(c *cli.Context) error {
+ defExecutionContainerWebApps().
+ webAppswithServerForExe3(utl.ConvGasToRun(c), c).
+ dispResult(c)
+ return nil
+}
+
+// downloadFiles : Download files from Google Drive.
+func downloadFiles(c *cli.Context) error {
+ res := defAuthContainer(c).
+ ggsrunIni(c).
+ goauth().
+ defDownloadContainer(c).
+ GetFileinf().
+ Downloader(c)
+ dispTransferResult(c, res)
+ return nil
+}
+
+// uploadFiles :
+func uploadFiles(c *cli.Context) error {
+ res := defAuthContainer(c).
+ ggsrunIni(c).
+ goauth().
+ defUploadContainer(c).
+ Uploader(c)
+ dispTransferResult(c, res)
+ return nil
+}
+
+// showFileList :
+func showFileList(c *cli.Context) error {
+ res := defAuthContainer(c).
+ ggsrunIni(c).
+ goauth().
+ defDownloadContainer(c).
+ GetFileList(c)
+ dispTransferResult(c, res)
+ return nil
+}
+
+// reAuth : Retrieve tokens again.
+func reAuth(c *cli.Context) error {
+ defAuthContainer(c).
+ ggsrunIni(c).
+ reAuth()
+ fmt.Print("Done.")
+ return nil
+}
+
+// dispResult : Display result
+func (e *ExecutionContainer) dispResult(c *cli.Context) {
+ var dispRes []byte
+ if len(e.Msg) > 0 {
+ e.FeedBackData.Response.Result.Message = e.Msg
+ }
+ if c.Bool("jsonparser") {
+ dispRes, _ = json.MarshalIndent(e.FeedBackData.Response.Result, "", " ")
+ } else {
+ dispRes, _ = json.Marshal(e.FeedBackData.Response.Result)
+ }
+ if c.Bool("onlyresult") {
+ if c.Bool("jsonparser") {
+ onlyres, _ := json.MarshalIndent(e.FeedBackData.Response.Result.Result, "", " ")
+ fmt.Printf("%s\n", string(onlyres))
+ } else {
+ onlyres, _ := json.Marshal(e.FeedBackData.Response.Result.Result)
+ fmt.Printf("%s\n", string(onlyres))
+ }
+ } else {
+ fmt.Printf("%v\n", string(dispRes))
+ }
+}
+
+// dispTransferResult : Display result
+func dispTransferResult(c *cli.Context, f *utl.FileInf) {
+ var dispRes []byte
+ if c.Bool("jsonparser") {
+ dispRes, _ = json.MarshalIndent(f, "", " ")
+ } else {
+ dispRes, _ = json.Marshal(f)
+ }
+ fmt.Printf("%s\n", string(dispRes))
+}
+
+// commandNotFound :
+func commandNotFound(c *cli.Context, command string) {
+ fmt.Fprintf(os.Stderr, "'%s' is not a %s command. Check '%s --help' or '%s -h'.", command, c.App.Name, c.App.Name, c.App.Name)
+ os.Exit(2)
+}
diff --git a/init.go b/init.go
new file mode 100644
index 0000000..2589623
--- /dev/null
+++ b/init.go
@@ -0,0 +1,58 @@
+// Package main (init.go) :
+// These methods are for reading and writing configuration file (ggsrun.cfg).
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ "github.com/urfave/cli"
+)
+
+// GgsrunIni :
+func (a *AuthContainer) ggsrunIni(c *cli.Context) *AuthContainer {
+ if cfgdata, err := ioutil.ReadFile(filepath.Join(a.InitVal.workdir, cfgFile)); err == nil {
+ err = json.Unmarshal(cfgdata, &a.GgsrunCfg)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error: Format error of '%s'. ", cfgFile)
+ os.Exit(1)
+ }
+ if c.Command.Names()[0] == "exe1" ||
+ c.Command.Names()[0] == "exe2" {
+ if len(c.String("scriptid")) == 0 && len(a.GgsrunCfg.Scriptid) == 0 {
+ fmt.Fprintf(os.Stderr, "Error: No script id. Please use option '-i [Script ID]'. ")
+ os.Exit(1)
+ }
+ if len(c.String("scriptid")) > 0 {
+ a.GgsrunCfg.Scriptid = c.String("scriptid")
+ a.InitVal.update = true
+ }
+ if len(c.String("function")) > 0 {
+ a.Param.Function = c.String("function")
+ }
+ }
+ } else {
+ return a.readClientSecret()
+ }
+ return a
+}
+
+func (a *AuthContainer) readClientSecret() *AuthContainer {
+ if csecret, err := ioutil.ReadFile(filepath.Join(a.InitVal.workdir, clientsecretFile)); err == nil {
+ err := json.Unmarshal(csecret, &a.Cs)
+ if err != nil || (len(a.Cs.Cid.ClientID) == 0 && len(a.Cs.Ciw.ClientID) == 0) {
+ fmt.Fprintf(os.Stderr, "Error: Please confirm '%s'. Error is %s.", clientsecretFile, err)
+ os.Exit(1)
+ }
+ if len(a.Cs.Cid.ClientID) == 0 && len(a.Cs.Ciw.ClientID) > 0 {
+ a.Cs.Cid = a.Cs.Ciw
+ }
+ } else {
+ fmt.Fprintf(os.Stderr, "Error: No materials for retrieving accesstoken. Please download '%s'", clientsecretFile)
+ os.Exit(1)
+ }
+ return a
+}
diff --git a/materials.go b/materials.go
new file mode 100644
index 0000000..c9eb042
--- /dev/null
+++ b/materials.go
@@ -0,0 +1,341 @@
+// Package main (materials.go) :
+// Materials for ggsrun.
+package main
+
+import (
+ "path/filepath"
+ "regexp"
+ "time"
+
+ "github.com/tanaikech/ggsrun/utl"
+ "github.com/urfave/cli"
+)
+
+// const :
+const (
+ appname = "ggsrun"
+ serverid = "115-19njNHlbT-NI0hMPDnVO1sdrw2tJKCAJgOTIAPbi_jq3tOo4lVRov"
+
+ eapir1 = "Execution API without server"
+ eapir2 = "Execution API with server"
+ wapps = "Web Apps with server"
+
+ clientsecretFile = "client_secret.json"
+ cfgFile = "ggsrun.cfg"
+
+ deffuncserv = "ggsrunif.ExecutionApi"
+ deffuncwith = "main"
+ deffuncwithout = "main"
+ defprojectname = appname
+ defPort = 8080
+
+ oauthurl = "https://accounts.google.com/o/oauth2/"
+ sdownloadurl = "https://script.google.com/feeds/download/export?id="
+ executionurl = "https://script.googleapis.com/v1/scripts/"
+ driveapiurl = "https://www.googleapis.com/drive/v3/files/"
+ chkatutl = "https://www.googleapis.com/oauth2/v3/"
+ uploadurl = "https://www.googleapis.com/upload/drive/v3/files/"
+)
+
+// InitVal : Initial values
+type InitVal struct {
+ pstart time.Time
+ workdir string
+ update bool
+ log bool
+ Port int
+}
+
+// ResMsg : Response message also included errors
+type ResMsg struct {
+ Msg []string
+}
+
+// GgsrunCfg : Configuration file for ggsrun
+type GgsrunCfg struct {
+ Scriptid string `json:"script_id"`
+ Clientid string `json:"client_id"`
+ Clientsecret string `json:"client_secret"`
+ Refreshtoken string `json:"refresh_token"`
+ Accesstoken string `json:"access_token,omitempty"`
+ Expiresin int64 `json:"expires_in,omitempty"`
+ Scopes []string `json:"scopes"`
+}
+
+// Cinstalled : File of client-secret.json
+type Cinstalled struct {
+ ClientID string `json:"client_id"`
+ Projectid string `json:"project_id"`
+ Authuri string `json:"auth_uri"`
+ Tokenuri string `json:"token_uri"`
+ Authproviderx509certurl string `json:"auth_provider_x509_cert_url"`
+ Clientsecret string `json:"client_secret"`
+ Redirecturis []string `json:"redirect_uris"`
+}
+
+// Cs : Client_secret.json
+type Cs struct {
+ Cid Cinstalled `json:"installed,omitempty"`
+ Ciw Cinstalled `json:"web,omitempty"`
+}
+
+// Atoken : Accesstoken given from Google
+type Atoken struct {
+ Accesstoken string `json:"access_token"`
+ Refreshtoken string `json:"refresh_token"`
+ Expiresin int64 `json:"expires_in"`
+}
+
+// ChkAt : Condition of accesstoken retrieved using Drive API
+type ChkAt struct {
+ Azu string `json:"azu,omitempty"`
+ Aud string `json:"aud,omitempty"`
+ Scope string `json:"scope,omitempty"`
+ Exp string `json:"exp,omitempty"`
+ Expiresin string `json:"expires_in,omitempty"`
+ Accesstype string `json:"access_type,omitempty"`
+ Error string `json:"error_description,omitempty"`
+}
+
+// Param : Payload for Execution API
+type Param struct {
+ Function string `json:"function"`
+ Parameters []string `json:"parameters,omitempty"`
+ DevMode bool `json:"devMode"`
+}
+
+// e1para : Parameter for exe1
+type e1para struct {
+ Function string `json:"function"`
+ Parameters []interface{} `json:"parameters,omitempty"`
+ DevMode bool `json:"devMode"`
+}
+
+// Com : Structure of data using Execution API
+type Com struct {
+ Com string `json:"com,omitempty"`
+ Exefunc string `json:"exefunc,omitempty"`
+ Log bool `json:"log"`
+}
+
+// Project : Project for uploading using Drive API
+type Project struct {
+ Files []File `json:"files"`
+}
+
+// File : Individual file in a project
+type File struct {
+ ID string `json:"id,omitempty"`
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Source string `json:"source"`
+}
+
+// FeedBackData : Feedbacked data from function using Execution API (modified)
+type FeedBackData struct {
+ Name string `json:"name"`
+ Done bool `json:"done"`
+ Response struct {
+ Type string `json:"@type"`
+ Result Resvalue `json:"result"`
+ } `json:"response"`
+ Error struct {
+ Code int `json:"code,omitempty"`
+ Message string `json:"message,omitempty"`
+ Status string `json:"status,omitempty"`
+ Detailes []ErrorMsg `json:"details,omitempty"`
+ } `json:"error"`
+}
+
+// ErrorMsg :
+type ErrorMsg struct {
+ Type string `json:"@type,omitempty"`
+ ErrorMessage string `json:"errorMessage,omitempty"`
+ ErrorType string `json:"errorType,omitempty"`
+ ScriptStackTraceElements []struct {
+ Function string `json:"function,omitempty"`
+ LineNumber int `json:"lineNumber,omitempty"`
+ } `json:"scriptStackTraceElements,omitempty"`
+}
+
+// Resvalue : Results of ggsrun
+type Resvalue struct {
+ Result interface{} `json:"result"`
+ Logger []interface{} `json:"logger,omitempty"`
+ GoogleEt float64 `json:"GoogleElapsedTime,omitempty"`
+ TotalEt float64 `json:"TotalElapsedTime,omitempty"`
+ Date string `json:"ScriptDate,omitempty"`
+ Uapi string `json:"API,omitempty"`
+ Message []string `json:"message,omitempty"`
+}
+
+// DlFileByScript : Information of download file by script
+type DlFileByScript struct {
+ Fileid string `json:"-"`
+ Extension string `json:"-"`
+}
+
+// ProjectUpdaterMeta : Metadata for updating a project
+type ProjectUpdaterMeta struct {
+ MimeType string `json:"mimeType"`
+}
+
+// ByteSliceFile : File with byte slice
+type ByteSliceFile struct {
+ FileData []int `json:"result"`
+ Name string `json:"name"`
+ MimeType string `json:"mimeType"`
+}
+
+// serverInfToGetCode : For getting auth code
+type serverInfToGetCode struct {
+ Response chan authCode
+ Start chan bool
+ End chan bool
+}
+
+// authCode : For getting auth code
+type authCode struct {
+ Code string
+ Err error
+}
+
+// AuthContainer : Struct container for using OAuth2
+type AuthContainer struct {
+ *InitVal // Initial values
+ *ResMsg // Response message
+ *GgsrunCfg // Config for ggsrun
+ *Param // Payload for Execution API
+ *Cs // Client_secret.json
+ *Atoken // Accesstoken from Google
+ *ChkAt // Check accesstoken
+}
+
+// ExecutionContainer : Struct container for using Execution API.
+// 1. Upload script using Execution API
+// 2. Update project using Drive API and execute script using Execution API
+type ExecutionContainer struct {
+ *InitVal // Initial values
+ *ResMsg // Response message
+ *GgsrunCfg // Config for ggsrun
+ *Param // Payload for Execution API
+ *FeedBackData // Feedbacked data from function using Execution API
+ *Project // Project for uploading using Drive API
+ *DlFileByScript // Information of download file by script
+}
+
+// DefAuthContainer : Struct container for authorization
+func defAuthContainer(c *cli.Context) *AuthContainer {
+ var err error
+ a := &AuthContainer{
+ &InitVal{},
+ &ResMsg{},
+ &GgsrunCfg{},
+ &Param{},
+ &Cs{},
+ &Atoken{},
+ &ChkAt{},
+ }
+ a.InitVal.pstart = time.Now()
+ a.InitVal.workdir, err = filepath.Abs(".")
+ if err != nil {
+ panic(err)
+ }
+ a.Param.Function = c.String("function")
+ a.InitVal.log = c.Bool("log")
+ a.InitVal.Port = defPort
+ if c.Command.Names()[0] == "auth" {
+ if c.Int("port") != 0 {
+ a.InitVal.Port = c.Int("port")
+ }
+ }
+ // Default scopes for using Execution API and Drive API
+ // If you want to use own scopes, please write them to configuration file.
+ // They are used for retrieving access token.
+ a.GgsrunCfg.Scopes = []string{
+ "https://www.googleapis.com/auth/drive",
+ "https://www.googleapis.com/auth/drive.file",
+ "https://www.googleapis.com/auth/drive.scripts",
+ "https://www.googleapis.com/auth/script.external_request",
+ "https://www.googleapis.com/auth/script.scriptapp",
+ "https://www.googleapis.com/auth/spreadsheets",
+ }
+ return a
+}
+
+// DefExecutionContainer : Struct container for using Execution API
+func (a *AuthContainer) defExecutionContainer() *ExecutionContainer {
+ e := &ExecutionContainer{
+ &InitVal{},
+ &ResMsg{},
+ &GgsrunCfg{},
+ &Param{},
+ &FeedBackData{},
+ &Project{},
+ &DlFileByScript{},
+ }
+ e.GgsrunCfg = a.GgsrunCfg
+ e.InitVal = a.InitVal
+ e.Msg = a.Msg
+ e.Param = a.Param
+ return e
+}
+
+// DefExecutionContainerWebApps : Struct container for using WebApps
+func defExecutionContainerWebApps() *ExecutionContainer {
+ var err error
+ e := &ExecutionContainer{
+ &InitVal{},
+ &ResMsg{},
+ &GgsrunCfg{},
+ &Param{},
+ &FeedBackData{},
+ &Project{},
+ &DlFileByScript{},
+ }
+ e.InitVal.pstart = time.Now()
+ e.InitVal.workdir, err = filepath.Abs(".")
+ if err != nil {
+ panic(err)
+ }
+ return e
+}
+
+// DefDownloadContainer : Struct container for downloading files
+func (a *AuthContainer) defDownloadContainer(c *cli.Context) *utl.FileInf {
+ p := &utl.FileInf{
+ Msgar: a.Msg,
+ Accesstoken: a.GgsrunCfg.Accesstoken,
+ Workdir: a.InitVal.workdir,
+ PstartTime: a.InitVal.pstart,
+ FileID: c.String("fileid"),
+ WantExt: c.String("extension"),
+ WantName: c.String("filename"),
+ }
+ return p
+}
+
+// DefUploadContainer : Struct container for uploading files
+func (a *AuthContainer) defUploadContainer(c *cli.Context) *utl.FileInf {
+ p := &utl.FileInf{
+ Msgar: a.Msg,
+ Accesstoken: a.GgsrunCfg.Accesstoken,
+ Workdir: a.InitVal.workdir,
+ PstartTime: a.InitVal.pstart,
+ UpFilename: regexp.MustCompile(`\s*,\s*`).Split(c.String("filename"), -1),
+ }
+ return p
+}
+
+// defDownloadByScriptContainer : Struct container for downloading files by GAS
+func (e *ExecutionContainer) defDownloadByScriptContainer() *utl.FileInf {
+ p := &utl.FileInf{
+ Msgar: e.Msg,
+ Accesstoken: e.GgsrunCfg.Accesstoken,
+ Workdir: e.InitVal.workdir,
+ PstartTime: e.InitVal.pstart,
+ FileID: e.DlFileByScript.Fileid,
+ WantExt: e.DlFileByScript.Extension,
+ }
+ return p
+}
diff --git a/oauth.go b/oauth.go
new file mode 100644
index 0000000..4ea54f5
--- /dev/null
+++ b/oauth.go
@@ -0,0 +1,254 @@
+// Package main (oauth.go) :
+// Get accesstoken using refreshtoken, and confirm condition of accesstoken.
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "net"
+ "net/http"
+ "net/url"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/tanaikech/ggsrun/utl"
+)
+
+// Goauth :
+func (a *AuthContainer) goauth() *AuthContainer {
+ if len(a.GgsrunCfg.Clientid) > 0 &&
+ len(a.GgsrunCfg.Clientsecret) > 0 &&
+ len(a.GgsrunCfg.Refreshtoken) > 0 {
+ if (a.InitVal.pstart.Unix()-a.GgsrunCfg.Expiresin) > 0 ||
+ len(a.GgsrunCfg.Accesstoken) == 0 {
+ a.getAtoken().makecfgfile()
+ } else {
+ if a.InitVal.update {
+ a.makecfgfile()
+ }
+ }
+ } else {
+ a.readClientSecret().getNewAccesstoken().makecfgfile()
+ }
+ return a
+}
+
+// ReAuth :
+func (a *AuthContainer) reAuth() {
+ a.readClientSecret().getNewAccesstoken().makecfgfile()
+}
+
+// makecfgfile :
+func (a *AuthContainer) makecfgfile() {
+ btok, _ := json.MarshalIndent(a.GgsrunCfg, "", "\t")
+ ioutil.WriteFile(filepath.Join(a.InitVal.workdir, cfgFile), btok, 0777)
+}
+
+// getAtoken : Retrieves accesstoken from refreshtoken.
+func (a *AuthContainer) getAtoken() *AuthContainer {
+ a.Msg = append(a.Msg, "Got a new accesstoken.")
+ values := url.Values{}
+ values.Set("client_id", a.GgsrunCfg.Clientid)
+ values.Set("client_secret", a.GgsrunCfg.Clientsecret)
+ values.Set("refresh_token", a.GgsrunCfg.Refreshtoken)
+ values.Set("grant_type", "refresh_token")
+ r := &utl.RequestParams{
+ Method: "POST",
+ APIURL: oauthurl + "token",
+ Data: strings.NewReader(values.Encode()),
+ Contenttype: "application/x-www-form-urlencoded",
+ Accesstoken: "",
+ Dtime: 10,
+ }
+ body, err := r.FetchAPI()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error: %v. ", err)
+ os.Exit(1)
+ }
+ json.Unmarshal(body, &a.Atoken)
+ a.GgsrunCfg.Accesstoken = a.Atoken.Accesstoken
+ a.GgsrunCfg.Expiresin = a.chkAtoken() - 360 // 6 minutes as adjustment time
+ return a
+}
+
+// chkAtoken : For AuthContainer
+func (a *AuthContainer) chkAtoken() int64 {
+ r := &utl.RequestParams{
+ Method: "GET",
+ APIURL: chkatutl + "tokeninfo?access_token=" + a.GgsrunCfg.Accesstoken,
+ Data: nil,
+ Contenttype: "application/x-www-form-urlencoded",
+ Accesstoken: "",
+ Dtime: 10,
+ }
+ body, err := r.FetchAPI()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error: %v. ", err)
+ os.Exit(1)
+ }
+ json.Unmarshal(body, &a.ChkAt)
+ if len(a.ChkAt.Error) > 0 {
+ a.getAtoken()
+ }
+ exp, _ := strconv.ParseInt(a.ChkAt.Exp, 10, 64)
+ return exp
+}
+
+// chkAtoken : For ExecutionContainer
+func (e *ExecutionContainer) chkAtoken() *ChkAt {
+ r := &utl.RequestParams{
+ Method: "GET",
+ APIURL: chkatutl + "tokeninfo?access_token=" + e.GgsrunCfg.Accesstoken,
+ Data: nil,
+ Contenttype: "application/x-www-form-urlencoded",
+ Accesstoken: "",
+ Dtime: 10,
+ }
+ body, _ := r.FetchAPI()
+ var c ChkAt
+ json.Unmarshal(body, &c)
+ return &c
+}
+
+func (a *AuthContainer) chkRedirectURI() bool {
+ for _, e := range a.Cs.Cid.Redirecturis {
+ if strings.Contains(e, "localhost") {
+ return true
+ }
+ }
+ return false
+}
+
+func (a *AuthContainer) getCode() (string, error) {
+ p := a.InitVal.Port
+ if !a.chkRedirectURI() {
+ return "", fmt.Errorf("Go manual mode.")
+ }
+ fmt.Printf("\n### This is a automatic input mode.\n### Please follow opened browser, login Google and click authentication.\n### Moves to a manual mode if you wait for 30 seconds under this situation.\n")
+ a.Cs.Cid.Redirecturis = append(a.Cs.Cid.Redirecturis, "http://localhost:"+strconv.Itoa(p)+"/")
+ codepara := url.Values{}
+ codepara.Set("client_id", a.Cs.Cid.ClientID)
+ codepara.Set("redirect_uri", a.Cs.Cid.Redirecturis[len(a.Cs.Cid.Redirecturis)-1])
+ codepara.Set("scope", strings.Join(a.GgsrunCfg.Scopes, " "))
+ codepara.Set("response_type", "code")
+ codepara.Set("approval_prompt", "force")
+ codepara.Set("access_type", "offline")
+ codeurl := oauthurl + "auth?" + codepara.Encode()
+ s := &serverInfToGetCode{
+ Response: make(chan authCode, 1),
+ Start: make(chan bool, 1),
+ End: make(chan bool, 1),
+ }
+ defer func() {
+ s.End <- true
+ }()
+ go func(port int) {
+ mux := http.NewServeMux()
+ mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ code := r.URL.Query().Get("code")
+ if len(code) == 0 {
+ fmt.Fprintf(w, `
Erorr.
`) + s.Response <- authCode{Err: fmt.Errorf("Not found code.")} + return + } + fmt.Fprintf(w, `The authentication was done. Please close this page.
`) + s.Response <- authCode{Code: code} + }) + var err error + Listener, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port)) + if err != nil { + s.Response <- authCode{Err: err} + return + } + server := http.Server{} + server.Handler = mux + go server.Serve(Listener) + s.Start <- true + <-s.End + Listener.Close() + s.Response <- authCode{Err: err} + return + }(p) + <-s.Start + var cmd *exec.Cmd + switch runtime.GOOS { + case "darwin": + cmd = exec.Command("open", strings.Replace(codeurl, "&", `\&`, -1)) + case "linux": + cmd = exec.Command("xdg-open", strings.Replace(codeurl, "&", `\&`, -1)) + case "windows": + cmd = exec.Command("cmd", "/c", "start", strings.Replace(codeurl, "&", `^&`, -1)) + default: + return "", fmt.Errorf("Go manual mode.") + } + if err := cmd.Start(); err != nil { + return "", fmt.Errorf("Go manual mode.") + } + var result authCode + select { + case result = <-s.Response: + case <-time.After(time.Duration(30) * time.Second): // After 30 s, move to manual mode. + return "", fmt.Errorf("Go manual mode.") + } + if result.Err != nil { + return "", fmt.Errorf("Go manual mode.") + } + return result.Code, nil +} + +// getNewAccesstoken : Retrieve accesstoken when there is no refreshtoken. +func (a *AuthContainer) getNewAccesstoken() *AuthContainer { + var code string + var err error + code, err = a.getCode() + if err != nil { + codepara := url.Values{} + codepara.Set("client_id", a.Cs.Cid.ClientID) + codepara.Set("redirect_uri", a.Cs.Cid.Redirecturis[0]) + codepara.Set("scope", strings.Join(a.GgsrunCfg.Scopes, " ")) + codepara.Set("response_type", "code") + codepara.Set("approval_prompt", "force") + codepara.Set("access_type", "offline") + codeurl := oauthurl + "auth?" + codepara.Encode() + fmt.Printf("\n### This is a manual input mode.\n### Please input code retrieved by importing following URL to your browser.\n\n"+ + "[URL]==> %v\n"+ + "[CODE]==>", codeurl) + if _, err := fmt.Scan(&code); err != nil { + log.Fatalf("Error: %v.\n", err) + } + a.Cs.Cid.Redirecturis = append(a.Cs.Cid.Redirecturis, a.Cs.Cid.Redirecturis[0]) + } + tokenparams := url.Values{} + tokenparams.Set("client_id", a.Cs.Cid.ClientID) + tokenparams.Set("client_secret", a.Cs.Cid.Clientsecret) + tokenparams.Set("redirect_uri", a.Cs.Cid.Redirecturis[len(a.Cs.Cid.Redirecturis)-1]) + tokenparams.Set("code", code) + tokenparams.Set("grant_type", "authorization_code") + r := &utl.RequestParams{ + Method: "POST", + APIURL: oauthurl + "token", + Data: strings.NewReader(tokenparams.Encode()), + Contenttype: "application/x-www-form-urlencoded", + Accesstoken: "", + Dtime: 10, + } + body, err := r.FetchAPI() + if err != nil { + fmt.Fprintf(os.Stderr, "Error: [ %v ] - Code is wrong. ", err) + os.Exit(1) + } + json.Unmarshal(body, &a.Atoken) + a.GgsrunCfg.Clientid = a.Cs.Cid.ClientID + a.GgsrunCfg.Clientsecret = a.Cs.Cid.Clientsecret + a.GgsrunCfg.Refreshtoken = a.Atoken.Refreshtoken + a.GgsrunCfg.Accesstoken = a.Atoken.Accesstoken + a.GgsrunCfg.Expiresin = a.chkAtoken() - 360 // 6 minutes as adjustment time + return a +} diff --git a/readme_Flow_exe1.png b/readme_Flow_exe1.png new file mode 100644 index 0000000..29c68fa Binary files /dev/null and b/readme_Flow_exe1.png differ diff --git a/readme_Flow_exe2.png b/readme_Flow_exe2.png new file mode 100644 index 0000000..6b816b8 Binary files /dev/null and b/readme_Flow_exe2.png differ diff --git a/readme_flow.png b/readme_flow.png new file mode 100644 index 0000000..d90a9f3 Binary files /dev/null and b/readme_flow.png differ diff --git a/readme_sheet.png b/readme_sheet.png new file mode 100644 index 0000000..475ca85 Binary files /dev/null and b/readme_sheet.png differ diff --git a/readme_sublimedemo.gif b/readme_sublimedemo.gif new file mode 100644 index 0000000..d035a26 Binary files /dev/null and b/readme_sublimedemo.gif differ diff --git a/readme_terminaldemo.gif b/readme_terminaldemo.gif new file mode 100644 index 0000000..3255b4a Binary files /dev/null and b/readme_terminaldemo.gif differ diff --git a/sender.go b/sender.go new file mode 100644 index 0000000..9b539dc --- /dev/null +++ b/sender.go @@ -0,0 +1,383 @@ +// Package main (sender.go) : +// These methods are for sending GAS scripts to Google Drive. +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "math" + "mime/multipart" + "net/textproto" + "net/url" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + "time" + + "github.com/tanaikech/ggsrun/utl" + "github.com/urfave/cli" +) + +// Exe1Function : +func (e *ExecutionContainer) exe1Function(c *cli.Context) *ExecutionContainer { + if len(c.String("scriptfile")) > 0 || c.Bool("backup") { + return e.projectBackup(c).projectUpdate(utl.ConvGasToPut(c)) + } + return e +} + +// Exe2Function : +func (e *ExecutionContainer) exe2Function(c *cli.Context) *ExecutionContainer { + if c.Bool("foldertree") { + btof := "function main(){return new ggsrun(null, null, null).foldertree()}" + return e.executionAPIwithServer(utl.ConvStringToRun(c, btof)).esenderForExe2(c) + } + if c.Bool("convert") && len(c.String("value")) > 0 { + btof := "function main(e){return new ggsrun(e, null, null).nodocsdownloader()}" + return e.executionAPIwithServer(utl.ConvStringToRun(c, btof)).esenderForExe2(c).byteSliceConverter() + } else if c.Bool("convert") && len(c.String("value")) == 0 { + fmt.Fprintf(os.Stderr, "Error: No File ID. Please set it using '-v [ File ID ]'. ") + os.Exit(1) + } + if len(c.String("stringscript")) > 0 { + return e.executionAPIwithServer(utl.ConvStringToRun(c, c.String("stringscript"))).esenderForExe2(c) + } + return e.executionAPIwithServer(utl.ConvGasToRun(c)).esenderForExe2(c) +} + +// ExecutionAPIwithoutServer : +func (e *ExecutionContainer) executionAPIwithoutServer(c *cli.Context) *ExecutionContainer { + if len(e.Param.Function) == 0 { + e.Param.Function = deffuncwithout + e.Msg = append(e.Msg, fmt.Sprintf("Executed default function '%s()'.", deffuncwithout)) + } + e.Param.DevMode = true + return e +} + +// executionAPIwithServer : +func (e *ExecutionContainer) executionAPIwithServer(sendscript string) *ExecutionContainer { + if len(sendscript) == 0 { + fmt.Fprintf(os.Stderr, "Error: No script. Please set GAS script using '-s'. ") + os.Exit(1) + } + if len(e.Param.Function) == 0 { + e.Param.Function = deffuncserv + } + scr := &Com{ + Com: sendscript, + Exefunc: e.Param.Function, + Log: e.InitVal.log, + } + scri, _ := json.Marshal(scr) + e.Param.Parameters = []string{string(scri)} + e.Param.DevMode = true + return e +} + +// executionError : Check error for execution API +func (e *ExecutionContainer) executionError(body []byte, err error) { + if err != nil { + json.Unmarshal(body, &e.FeedBackData) + if e.FeedBackData.Error.Status == "UNAUTHENTICATED" { + if len(e.chkAtoken().Error) > 0 { + fmt.Printf("Invalid Access token. Please retrieve it again using command '%s auth'.\nCurrent access token is '%s'.\n", appname, e.GgsrunCfg.Accesstoken) + os.Exit(1) + } + fmt.Printf("Authorization Error: Please check SCOPEs of your GAS script and server using GAS Script Editor.\nIf the SCOPEs have changed, modify them in '%s' and delete a line of 'refresh_token', then, execute '%s' again. You can retrieve new access token with modified SCOPEs.\n", cfgFile, appname) + os.Exit(1) + } + if e.FeedBackData.Error.Message == "PERMISSION_DENIED" && + e.FeedBackData.Error.Code == 403 { + fmt.Printf("Error: Please check Execution API at Developer console.\nIf Execution API is unable, please enable it. Or please check 'client_secret.json'. It might be that that is not for the project with Execution API.\n") + os.Exit(1) + } + if e.FeedBackData.Error.Message == "Requested entity was not found." && + e.FeedBackData.Error.Code == 404 { + fmt.Printf("Error: Please check the deployment of API executable and/or the ggsrun server.\n - If you use command 'e1', please deploy API executable again. If you use command 'e2', please check both again.\n - After deployed API executable, please save each scripts on the project again. This is very important point!\n - When you use the server as library, please confirm server.\n - Also you can use 'Logger.log(ggsrunif.Beacon())' at Google Apps Script Editor to confirm server condition.\n - Also, please check the script ID.") + os.Exit(1) + } + if e.FeedBackData.Error.Detailes[0].ErrorMessage == "The script completed but the returned value is not a supported return type." && + e.FeedBackData.Error.Code == 500 { + fmt.Printf("Error: %s\n", e.FeedBackData.Error.Detailes[0].ErrorMessage) + os.Exit(1) + } + fmt.Fprintf(os.Stderr, "Error: %s.\n%s", err, body) + os.Exit(1) + } +} + +// MarshalJSON : For exe1 +func (e *e1para) MarshalJSON() ([]byte, error) { + var outd string + if len(e.Parameters) > 0 { + if regexp.MustCompile("^[+-]?[0-9]*[\\.]?[0-9]+$").Match([]byte(e.Parameters[0].(string))) || + regexp.MustCompile("^\\[|\\]$").Match([]byte(e.Parameters[0].(string))) || + regexp.MustCompile("^{|}$").Match([]byte(e.Parameters[0].(string))) { + outd = fmt.Sprintf("{\"devMode\":%t, \"parameters\":%v, \"function\":%q}", e.DevMode, e.Parameters, e.Function) + } else if regexp.MustCompile("([a-zA-Z]|[0-9].*[a-zA-Z]|[a-zA-Z].*[0-9])").Match([]byte(e.Parameters[0].(string))) { + outd = fmt.Sprintf("{\"devMode\":%t, \"parameters\":%q, \"function\":%q}", e.DevMode, e.Parameters, e.Function) + } + } else { + outd = fmt.Sprintf("{\"devMode\":%t, \"function\":%q}", e.DevMode, e.Function) + } + return []byte(outd), nil +} + +// EsenderForExe1 : Sends GAS to Google and retrieves results. +func (e *ExecutionContainer) esenderForExe1(c *cli.Context) *ExecutionContainer { + var paraint []interface{} + if len(c.String("value")) > 0 { + paraint = []interface{}{c.String("value")} + } + epara := &e1para{ + Function: e.Param.Function, + Parameters: paraint, + DevMode: e.Param.DevMode, + } + re, _ := json.Marshal(epara) + if len(re) == 0 { + fmt.Fprintf(os.Stderr, "Error: Format of values is wrong. Double and single quotates have to be escaped.\n - Inputted value was %s\n", c.String("value")) + os.Exit(1) + } + r := &utl.RequestParams{ + Method: "POST", + APIURL: executionurl + e.GgsrunCfg.Scriptid + ":run", + Data: bytes.NewBuffer(re), + Contenttype: "application/json;charset=UTF-8", + Accesstoken: e.GgsrunCfg.Accesstoken, + Dtime: 370, + } + body, err := r.FetchAPI() + e.executionError(body, err) + json.Unmarshal(body, &e.FeedBackData) + var dat string + if len(e.FeedBackData.Error.Message) > 0 { + if len(e.FeedBackData.Error.Detailes[0].ScriptStackTraceElements) > 0 { + dat = fmt.Sprintf("{code: %d, message: %s, function: %s, linenumber: %d}", e.FeedBackData.Error.Code, e.FeedBackData.Error.Message, e.FeedBackData.Error.Detailes[0].ScriptStackTraceElements[0].Function, e.FeedBackData.Error.Detailes[0].ScriptStackTraceElements[0].LineNumber) + } else { + dat = fmt.Sprintf("{code: %d, message: %s}", e.FeedBackData.Error.Code, e.FeedBackData.Error.Message) + } + e.Msg = append(e.Msg, dat) + } else { + var rs map[string]interface{} + json.Unmarshal(body, &rs) + e.FeedBackData.Response.Result.Result = rs["response"].(map[string]interface{})["result"] + } + if len(e.FeedBackData.Error.Detailes) > 0 { + dat = fmt.Sprintf("{detailmessage: %s}", e.FeedBackData.Error.Detailes[0].ErrorMessage) + e.Msg = append(e.Msg, dat) + } + e.FeedBackData.Response.Result.TotalEt = math.Trunc(time.Now().Sub(e.InitVal.pstart).Seconds()*1000) / 1000 + e.FeedBackData.Response.Result.Uapi = eapir1 + e.Msg = append(e.Msg, fmt.Sprintf("Function '%s()' was run.", e.Param.Function)) + return e +} + +// esenderForExe2 : Sends GAS to Google and retrieves results. +func (e *ExecutionContainer) esenderForExe2(c *cli.Context) *ExecutionContainer { + re, _ := json.Marshal(e.Param) + r := &utl.RequestParams{ + Method: "POST", + APIURL: executionurl + e.GgsrunCfg.Scriptid + ":run", + Data: bytes.NewBuffer(re), + Contenttype: "application/json;charset=UTF-8", + Accesstoken: e.GgsrunCfg.Accesstoken, + Dtime: 370, + } + body, err := r.FetchAPI() + e.executionError(body, err) + json.Unmarshal(body, &e.FeedBackData) + var dat string + if len(e.FeedBackData.Error.Message) > 0 { + dat = fmt.Sprintf("{code: %d, message: %s}", e.FeedBackData.Error.Code, e.FeedBackData.Error.Message) + e.Msg = append(e.Msg, dat) + } + if len(e.FeedBackData.Error.Detailes) > 0 { + if strings.Contains(e.FeedBackData.Error.Detailes[0].ErrorMessage, deffuncserv) { + dat = fmt.Sprintf("{server_error: Server for ggsrun is NOT found. Please deploy the server which is a library for GAS as 'ggsrunif'. Sctipt ID of the library is '%s'.}", serverid) + } else { + dat = fmt.Sprintf("{detailmessage: %s}", e.FeedBackData.Error.Detailes[0].ErrorMessage) + } + e.Msg = append(e.Msg, dat) + return e + } + e.FeedBackData.Response.Result.TotalEt = math.Trunc(time.Now().Sub(e.InitVal.pstart).Seconds()*1000) / 1000 + e.FeedBackData.Response.Result.Uapi = eapir2 + dlfileinf, _ := json.Marshal(e.FeedBackData.Response.Result.Result) + var rs map[string]interface{} + if err := json.Unmarshal(dlfileinf, &rs); err == nil { + fid, ok := rs["fileid"].(string) + if ok { + e.DlFileByScript.Fileid = fid + } + exn, ok := rs["extension"].(string) + if ok { + e.DlFileByScript.Extension = exn + } + if len(fid) > 0 && len(exn) > 0 { + delete(rs, "fileid") + delete(rs, "extension") + e.FeedBackData.Response.Result.Result = rs + res := e.defDownloadByScriptContainer(). + GetFileinf(). + Downloader(c) + e.Msg = append(e.Msg, res.Msgar...) + } + } + e.Msg = append(e.Msg, fmt.Sprintf("'%s()' in the script was run using ggsrun server. Server function is '%s()'.", deffuncwith, e.Param.Function)) + return e +} + +// ProjectUpdate : +func (e *ExecutionContainer) projectUpdate(sendscript string) *ExecutionContainer { + var overwrite bool + for i := range e.Project.Files { + if e.Project.Files[i].Name == defprojectname { + e.Project.Files[i].Source = sendscript + overwrite = true + } + } + if !overwrite { + filedata := &File{ + Name: defprojectname, + Type: "server_js", + Source: sendscript, + } + e.Project.Files = append(e.Project.Files, *filedata) + } + script, _ := json.Marshal(e.Project) + metadata, _ := json.Marshal(&ProjectUpdaterMeta{MimeType: "application/vnd.google-apps.script"}) + tokenparams := url.Values{} + tokenparams.Set("fields", "id,mimeType,name,parents") + var b bytes.Buffer + w := multipart.NewWriter(&b) + part := make(textproto.MIMEHeader) + part.Set("Content-Type", "application/json") + data, err := w.CreatePart(part) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v. ", err) + os.Exit(1) + } + if _, err = io.Copy(data, bytes.NewReader(metadata)); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v. ", err) + os.Exit(1) + } + data, err = w.CreatePart(part) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v. ", err) + os.Exit(1) + } + if _, err = io.Copy(data, bytes.NewReader(script)); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v. ", err) + os.Exit(1) + } + w.Close() + r := &utl.RequestParams{ + Method: "PATCH", + APIURL: uploadurl + e.GgsrunCfg.Scriptid + "?uploadType=multipart&" + tokenparams.Encode(), + Data: &b, + Contenttype: w.FormDataContentType(), + Accesstoken: e.GgsrunCfg.Accesstoken, + Dtime: 10, + } + res, err := r.FetchAPI() + if err != nil { + fmt.Fprintf(os.Stderr, "Error: Project cannot be updated. So the script cannot be executed. May your project be not a stand alone script? 'e1' command cannot be used to the stand alone script. Even if your script is a bound script, you can download a project using '-b' option.\n%v\n", err) + os.Exit(1) + } + e.Msg = append(e.Msg, "Project was updated.") + _ = res + return e +} + +// ProjectBackup : +func (e *ExecutionContainer) projectBackup(c *cli.Context) *ExecutionContainer { + r := &utl.RequestParams{ + Method: "GET", + APIURL: sdownloadurl + e.GgsrunCfg.Scriptid + "&format=json", + Data: nil, + Contenttype: "", + Accesstoken: e.GgsrunCfg.Accesstoken, + Dtime: 10, + } + res, err := r.FetchAPI() + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v. Please check script ID or access token. ", err) + os.Exit(1) + } + json.Unmarshal(res, &e.Project) + if c.Bool("backup") { + btok, _ := json.MarshalIndent(e.Project, "", "\t") + filename := e.InitVal.pstart.Format("20060102_150405") + ".gs" + ioutil.WriteFile(filepath.Join(e.InitVal.workdir, filename), btok, 0777) + dat := fmt.Sprintf("Project was saved as '%s'.", filename) + e.Msg = append(e.Msg, dat) + } + return e +} + +// WebAppswithServerForExe3 : Sends GAS to Google and retrieves results. +func (e *ExecutionContainer) webAppswithServerForExe3(script string, c *cli.Context) *ExecutionContainer { + if len(c.String("url")) == 0 { + fmt.Fprintf(os.Stderr, "Error: No URL for Web Apps.") + os.Exit(1) + } + tokenparams := url.Values{} + tokenparams.Set("com", script) + tokenparams.Set("pass", c.String("password")) + tokenparams.Set("log", strconv.FormatBool(c.Bool("log"))) + r := &utl.RequestParams{ + Method: "POST", + APIURL: c.String("url"), + Data: strings.NewReader(tokenparams.Encode()), + Contenttype: "application/x-www-form-urlencoded", + Accesstoken: "", + Dtime: 370, + } + body, err := r.FetchAPI() + if err != nil { + fmt.Fprintf(os.Stderr, "Error: Please check Web Apps Service and/or URL of it. Web Apps Service might not be deployed. ") + os.Exit(1) + } + json.Unmarshal(body, &e.FeedBackData.Response.Result) + e.FeedBackData.Response.Result.TotalEt = math.Trunc(time.Now().Sub(e.InitVal.pstart).Seconds()*1000) / 1000 + e.FeedBackData.Response.Result.Uapi = wapps + dlfileinf, _ := json.Marshal(e.FeedBackData.Response.Result.Result) + var rs map[string]interface{} + if err := json.Unmarshal(dlfileinf, &rs); err == nil { + e.DlFileByScript.Fileid, _ = rs["fileid"].(string) + e.DlFileByScript.Extension, _ = rs["extension"].(string) + if len(e.DlFileByScript.Fileid) > 0 && len(e.DlFileByScript.Extension) > 0 { + delete(rs, "fileid") + delete(rs, "extension") + e.FeedBackData.Response.Result.Result = rs + e.Msg = append(e.Msg, "This mode cannot download files. Because this mode is not authorization.") + } + } + return e +} + +// ByteSliceConverter : +func (e *ExecutionContainer) byteSliceConverter() *ExecutionContainer { + if !strings.Contains(fmt.Sprintf("%s", e.FeedBackData.Response.Result.Result), "Error") { + var f ByteSliceFile + rr, _ := json.Marshal(e.FeedBackData.Response.Result.Result) + json.Unmarshal(rr, &f) + c := make([]uint8, len(f.FileData)) + for n := range f.FileData { + c[n] = uint8(f.FileData[n]) + } + ioutil.WriteFile(f.Name, c, 0777) + e.FeedBackData.Response.Result.Result = "### Byte Slice of File ###" + e.Msg = append(e.Msg, fmt.Sprintf("File was downloaded as '%s'. MimeType is '%s'.", f.Name, f.MimeType)) + } else { + e.FeedBackData.Response.Result.Result = "Server isn't installed or Wrong File ID." + } + return e +} diff --git a/server/server.gs b/server/server.gs new file mode 100644 index 0000000..3042ae2 --- /dev/null +++ b/server/server.gs @@ -0,0 +1,225 @@ +var VERSION = "1.0.0"; +var IGGSRUN; + +/** + * This method is for ggsrun with Execution API. + * "ggsrun" brings us more convenient development-environment of Google Apps Script + * on local PC. "ggsrun" sends your script of Google Apps Script created at local PC + * to Google and retrieves the results from Google. This library "ggsrunif" is used + * for "ggsrun" made of golang as an InterFace. Namely, this is a server script. + * WEB https://github.com/tanaikech + *+ * $ ggsrun e2 -s [Script File] options + *+ * @return {string} Executed result of script. + */ +function ExecutionApi(e) { + IGGSRUN = new ggsrun(e, null, []); + return IGGSRUN.executionapi(); +} + +/** + * This method is for ggsrun with Web Apps. + * "ggsrun" brings us more convenient development-environment of Google Apps Script + * on local PC. "ggsrun" sends your script of Google Apps Script created at local PC + * to Google and retrieves the results from Google. This library "ggsrunif" is used + * for "ggsrun" made of golang as an InterFace. Namely, this is a server script. + * WEB https://github.com/tanaikech + *
+ * function doPost(e) { + * var password = "password"; // Password for using Web Apps + * return ggsrunif.WebApps(e, password); + * } + *+ * @param {e} Script and parameters. + * @param {password} Password for launching script sent from local PC. + * @return {string} Executed result of script. + */ +function WebApps(e, password) { + IGGSRUN = new ggsrun(e, password, []); + return IGGSRUN.webapps(); +// return new ggsrun(e, password).webapps(); +} + +/** + * This method is used to retrieve parameters as a log. Please use this in your script you send using ”ggsrun” instead of "Logger.log()". + * @param {Object} channelId Group to leave + * @return {Object} result + */ +function Log(l) { + IGGSRUN.logg(l); +} + +/** + * This method is a beacon to confirm the existence of ggsrun's server. + * WEB https://github.com/tanaikech + *
+ * ggsrunif.Beacon() + *+ * @return {string} Same string for sending string. + */ +function Beacon() { + return new ggsrun(null, null, null).beacon(); +} + +// Server script +(function(x) { + var ggsrun; + + ggsrun = (function() { + var emessage, wmessage, logsheet, recLog; + ggsrun.help = "This is a server script for ggsrun using Execution API and Web Apps."; + ggsrun.name = 'ggsrun'; + + function ggsrun(e, pass, logar) { + this.e = e; + this.pass = pass; + this.ss = logsheet("ggsrun.log"); + this.logar = logar; + } + + ggsrun.prototype.beacon = function() { + return "This is a server for ggsrun. Version is " + VERSION + ". Autor is https://github.com/tanaikech ."; + }; + + ggsrun.prototype.logg = function(val) { + this.logar.push(val); + }; + + ggsrun.prototype.nodocsdownloader = function() { + return (function(id){ + try { + var file = DriveApp.getFileById(id); + return { + result: file.getBlob().getBytes(), + name: file.getName(), + mimeType: file.getBlob().getContentType() + }; + } catch(err) { + return { + result: "Error" + }; + } + })(this.e); + }; + + ggsrun.prototype.foldertree = function() { + return (function(folder, folderSt, results){ + var ar = []; + var folders = folder.getFolders(); + while(folders.hasNext()) ar.push(folders.next()); + folderSt += folder.getName() + "(" + folder.getId() + ")#_aabbccddee_#"; + var array_folderSt = folderSt.split("#_aabbccddee_#"); + array_folderSt.pop() + results.push(array_folderSt); + ar.length == 0 && (folderSt = ""); + for (var i in ar) arguments.callee(ar[i], folderSt, results); + return results; + })(DriveApp.getRootFolder(), "", []); + }; + + ggsrun.prototype.executionapi = function() { + var startTime = Date.now(); + var dateDat = Utilities.formatDate(new Date(), "GMT", "yyyy-MM-dd_HH:mm:ss'_GMT'"); + var rec = JSON.parse(this.e); + if (rec.log) { + try { + recLog.call(this, [[ + dateDat, + "{API: \"Execution API\", ContentLength: " + this.e.length + ", ExecutedFunction: \"" + rec.exefunc + "()\"}", + rec.com + ]]); + } catch(err) { + var ss = err.message; // temporary + } + } + var res = ""; + try { + var resValues = (0,eval)((0,eval)(rec.com)); + res = emessage.call(this, rec.com ? resValues : "Error on GAS side: Bad parameters.", startTime, dateDat); + } catch(err) { + res = emessage.call(this, "Script Error on GAS side: " + err.message, startTime); + } + return res; + }; + + ggsrun.prototype.webapps = function() { + var startTime = Date.now(); + var dateDat = Utilities.formatDate(new Date(), "GMT", "yyyy-MM-dd_HH:mm:ss'_GMT'"); + if (this.e.parameters.log == "false" || !this.e.parameters.log) { + try { + recLog.call(this, [[ + dateDat, + "{API: \"Web Apps\", ContentLength: " + this.e.contentLength + ", Password: \"" + this.e.parameters.pass + "\"}", + this.e.parameters.com + ]]); + } catch(err) { + var ss = err.message; // temporary + } + } + var res = ""; + if (this.e.parameters.pass == this.pass) { + try { + var resValues = (0,eval)((0,eval)(this.e.parameters.com[0])); + res = wmessage.call(this, this.e.parameters.com ? resValues : "Error on GAS side: Bad parameters.", startTime, dateDat); + } catch(err) { + res = wmessage.call(this, "Script Error on GAS side: " + err.message, startTime); + } + } else { + res = wmessage.call(this, "Error on GAS side: Bad password.", startTime); + } + return res; + }; + + emessage = function(data, startTime, dateDat) { + return { + result: data, + logger: this.logar, + GoogleElapsedTime: ((Date.now() - startTime) / 1000), + ScriptDate: dateDat + }; + }; + + wmessage = function(data, startTime, dateDat) { + return ContentService + .createTextOutput(JSON.stringify({ + result: data, + logger: this.logar, + GoogleElapsedTime: ((Date.now() - startTime) / 1000), + ScriptDate: dateDat + })) + .setMimeType(ContentService.MimeType.JSON); + }; + + logsheet = function(_log) { + var logit = DriveApp.getFilesByName(_log); + var logar = []; + while (logit.hasNext()) { + logar.push(logit.next().getId()); + } + if (logar.length == 0) { + var ss = SpreadsheetApp.create(_log); + var logss = DriveApp.getFileById(ss.getId()); + try { + DriveApp.getFileById(ScriptApp.getScriptId()).getParents().next().addFile(logss); + } catch(e) { + DriveApp.getFileById(SpreadsheetApp.getActiveSpreadsheet().getId()).getParents().next().addFile(logss); + } + logss.getParents().next().removeFile(logss); + } else { + var ss = SpreadsheetApp.openById(logar[0]); + } + return ss.getSheets()[0]; + }; + + recLog = function(logAr) { + this.ss.getRange(this.ss.getLastRow() + 1, 1, logAr.length, logAr[0].length).setValues(logAr); + }; + + return ggsrun; + })(); + return x.ggsrun = ggsrun; +})(this); diff --git a/utl/converter.go b/utl/converter.go new file mode 100644 index 0000000..dc996ea --- /dev/null +++ b/utl/converter.go @@ -0,0 +1,232 @@ +// Package utl (convert.go) : +// This is a convereter to send GAS script to Google. +package utl + +import ( + "bufio" + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + + "github.com/urfave/cli" +) + +// const : +const ( + deffuncwith = "main" +) + +// chkScript : Check the imported script. +func chkScript(c *cli.Context) (string, error) { + var scriptfile string + fext := filepath.Ext(c.String("scriptfile")) + if fext == ".coffee" { + var cmd *exec.Cmd + cmd = exec.Command("coffee", "-cb", c.String("scriptfile")) + if err := cmd.Run(); err != nil { + return "", err + } + scriptfile = strings.Replace(filepath.Base(c.String("scriptfile")), fext, ".js", -1) + } else { + scriptfile = c.String("scriptfile") + } + return scriptfile, nil +} + +// ConvGasToPut : Reads GAS source and formats it. +func ConvGasToPut(c *cli.Context) string { + scriptfile, err := chkScript(c) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: Compile error of CoffeeScript. Please check the script.\n%s\n", err.Error()) + os.Exit(1) + } + var res string + if len(scriptfile) > 0 { + fp, err := os.Open(scriptfile) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: Script '%s' is not found. ", scriptfile) + os.Exit(1) + } + defer fp.Close() + scripts := []string{} + s := bufio.NewScanner(fp) + for s.Scan() { + dat := s.Text() + dat += "\n" + scripts = append(scripts, dat) + } + if s.Err() != nil { + fmt.Fprintf(os.Stderr, "Error: %v .", s.Err()) + os.Exit(1) + } + mem := make([]byte, 0, 100) + for _, v := range scripts { + mem = append(mem, v...) + } + res = string(mem) + } else { + res = "" + } + return res +} + +// ConvGasToRun : Reads GAS source and formats it. +func ConvGasToRun(c *cli.Context) string { + scriptfile, err := chkScript(c) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: Compile error of CoffeeScript. Please check the script.\n%s\n", err.Error()) + os.Exit(1) + } + senddata := c.String("value") + var res string + if len(scriptfile) > 0 { + fp, err := os.Open(scriptfile) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: Script '%s' is not found. ", scriptfile) + os.Exit(1) + } + defer fp.Close() + scripts := []string{} + s := bufio.NewScanner(fp) + for s.Scan() { + dat := s.Text() + if strings.Contains(dat, "//") { + if !strings.Contains(dat, "://") || strings.Contains(dat, "// ") || strings.Contains(dat, " //") { + dat = dat[:strings.Index(dat, "//")] + } + } + if dat != "" { + conv := strings.Replace(strings.TrimSpace(dat), "\\", "\\\\", -1) + conv = strings.Replace(strings.TrimSpace(conv), "\"", "\\\"", -1) + conv = strings.Replace(conv, "'", "\\'", -1) + scripts = append(scripts, conv) + } + } + if s.Err() != nil { + fmt.Fprintf(os.Stderr, "Error: %v .", s.Err()) + os.Exit(1) + } + mem := make([]byte, 0, 100) + for _, v := range scripts { + mem = append(mem, v...) + } + st := string(mem) + if len(senddata) > 0 { + if regexp.MustCompile("^[+-]?[0-9]*[\\.]?[0-9]+$").Match([]byte(senddata)) { + st += "var defdata=" + senddata + ";" + } else if regexp.MustCompile("^\\[|\\]$").Match([]byte(senddata)) || regexp.MustCompile("^{|}$").Match([]byte(senddata)) { + dat := strings.Replace(strings.TrimSpace(senddata), "\"", "\\\"", -1) + senddata = strings.Replace(dat, "'", "\\'", -1) + st += "var defdata=" + senddata + ";" + } else { + if regexp.MustCompile("^\"|\"$").Match([]byte(senddata)) || regexp.MustCompile("^\\'|\\'$").Match([]byte(senddata)) { + dat := strings.Replace(strings.TrimSpace(senddata), "\"", "\\\"", -1) + senddata = strings.Replace(dat, "'", "\\'", -1) + st += "var defdata=" + senddata + ";" + } else { + st += "var defdata=\\\"" + senddata + "\\\";" + } + } + st += deffuncwith + "(defdata)" + } else { + st += deffuncwith + "()" + } + res = "\"" + st + "\"" + } else { + res = "" + } + return res +} + +// ConvStringToRun : Reads GAS source and formats it as a string script. +func ConvStringToRun(c *cli.Context, stringscript string) string { + senddata := c.String("value") + var res string + if len(stringscript) > 0 { + scripts := []string{} + s := bufio.NewScanner(strings.NewReader(stringscript)) + for s.Scan() { + dat := s.Text() + if strings.Contains(dat, "//") { + if !strings.Contains(dat, "://") || strings.Contains(dat, "// ") || strings.Contains(dat, " //") { + dat = dat[:strings.Index(dat, "//")] + } + } + if dat != "" { + conv := strings.Replace(strings.TrimSpace(dat), "\\", "\\\\", -1) + conv = strings.Replace(strings.TrimSpace(conv), "\"", "\\\"", -1) + conv = strings.Replace(conv, "'", "\\'", -1) + scripts = append(scripts, conv) + } + } + if s.Err() != nil { + fmt.Fprintf(os.Stderr, "Error: %v .", s.Err()) + os.Exit(1) + } + mem := make([]byte, 0, 100) + for _, v := range scripts { + mem = append(mem, v...) + } + st := string(mem) + if len(senddata) > 0 { + if regexp.MustCompile("^[+-]?[0-9]*[\\.]?[0-9]+$").Match([]byte(senddata)) { + st += "var defdata=" + senddata + ";" + } else if regexp.MustCompile("^\\[|\\]$").Match([]byte(senddata)) || regexp.MustCompile("^{|}$").Match([]byte(senddata)) { + dat := strings.Replace(strings.TrimSpace(senddata), "\"", "\\\"", -1) + senddata = strings.Replace(dat, "'", "\\'", -1) + st += "var defdata=" + senddata + ";" + } else { + if regexp.MustCompile("^\"|\"$").Match([]byte(senddata)) || regexp.MustCompile("^\\'|\\'$").Match([]byte(senddata)) { + dat := strings.Replace(strings.TrimSpace(senddata), "\"", "\\\"", -1) + senddata = strings.Replace(dat, "'", "\\'", -1) + st += "var defdata=" + senddata + ";" + } else { + st += "var defdata=\\\"" + senddata + "\\\";" + } + } + st += deffuncwith + "(defdata)" + } else { + st += deffuncwith + "()" + } + res = "\"" + st + "\"" + } else { + res = "" + } + return res +} + +// ConvGasToUpload : +func ConvGasToUpload(scriptfile string) string { + var res string + if len(scriptfile) > 0 { + fp, err := os.Open(scriptfile) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: Script '%s' is not found. ", scriptfile) + os.Exit(1) + } + defer fp.Close() + scripts := []string{} + s := bufio.NewScanner(fp) + for s.Scan() { + dat := s.Text() + dat += "\n" + scripts = append(scripts, dat) + } + if s.Err() != nil { + fmt.Fprintf(os.Stderr, "Error: %v .", s.Err()) + os.Exit(1) + } + mem := make([]byte, 0, 100) + for _, v := range scripts { + mem = append(mem, v...) + } + res = string(mem) + } else { + res = "" + } + + return res +} diff --git a/utl/fetcher.go b/utl/fetcher.go new file mode 100644 index 0000000..f718b37 --- /dev/null +++ b/utl/fetcher.go @@ -0,0 +1,65 @@ +// Package utl (fetcher.go) : +// These methods are for retrieving data from URL. +package utl + +import ( + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "strconv" + "time" +) + +// RequestParams : Parameters for FetchAPI +type RequestParams struct { + Method string + APIURL string + Data io.Reader + Contenttype string + Accesstoken string + Dtime int64 +} + +// FetchAPI : For fetching data to URL. +func (r *RequestParams) FetchAPI() ([]byte, error) { + req, err := http.NewRequest( + r.Method, + r.APIURL, + r.Data, + ) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", r.Contenttype) + req.Header.Set("Authorization", "Bearer "+r.Accesstoken) + client := &http.Client{ + Timeout: time.Duration(r.Dtime) * time.Second, + } + res, err := client.Do(req) + if err != nil || res.StatusCode-300 >= 0 { + var msg []byte + var er string + if res == nil { + msg = []byte(err.Error()) + er = err.Error() + } else { + errmsg, err := ioutil.ReadAll(res.Body) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v. ", err) + os.Exit(1) + } + msg = errmsg + er = "Status Code: " + strconv.Itoa(res.StatusCode) + } + return msg, errors.New(er) + } + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, err + } + defer res.Body.Close() + return body, err +} diff --git a/utl/googlemimetypes.go b/utl/googlemimetypes.go new file mode 100644 index 0000000..53cd3c6 --- /dev/null +++ b/utl/googlemimetypes.go @@ -0,0 +1,244 @@ +package utl + +const ( + extVsmime = `{ + "js": "application/vnd.google-apps.script+json", + "gs": "application/vnd.google-apps.script+json", + "gas": "application/vnd.google-apps.script+json", + "csv": "text/csv", + "htm": "text/html", + "html": "text/html", + "xbm": "text/html", + "shtml": "text/html", + "shtm": "text/html", + "txt": "text/plain", + "text": "text/plain", + "json": "application/json", + "doc": "application/msword", + "xls": "application/vnd.ms-excel", + "ppt": "application/vnd.ms-powerpoint", + "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "pdf": "application/pdf", + "ps": "application/postscript", + "eps": "application/postscript", + "gif": "image/gif", + "png": "image/png", + "svg": "image/svg+xml", + "jpg": "image/jpeg", + "jpeg": "image/jpeg", + "bmp": "image/bmp", + "ico": "image/x-icon", + "tif": "image/tiff", + "tiff": "image/tiff", + "mp4": "video/mp4", + "zip": "application/zip" + }` + + googlemimetypes = `{ + "importFormats": { + "application/x-vnd.oasis.opendocument.presentation": [ + "application/vnd.google-apps.presentation" + ], + "text/tab-separated-values": [ + "application/vnd.google-apps.spreadsheet" + ], + "image/jpeg": [ + "image/jpeg" + ], + "image/bmp": [ + "image/bmp" + ], + "image/gif": [ + "image/gif" + ], + "application/vnd.ms-excel.sheet.macroenabled.12": [ + "application/vnd.google-apps.spreadsheet" + ], + "application/vnd.openxmlformats-officedocument.wordprocessingml.template": [ + "application/vnd.google-apps.document" + ], + "application/vnd.ms-powerpoint.presentation.macroenabled.12": [ + "application/vnd.google-apps.presentation" + ], + "application/vnd.ms-word.template.macroenabled.12": [ + "application/vnd.google-apps.document" + ], + "application/vnd.openxmlformats-officedocument.wordprocessingml.document": [ + "application/vnd.google-apps.document" + ], + "image/pjpeg": [ + "image/pjpeg" + ], + "application/vnd.google-apps.script+text/plain": [ + "application/vnd.google-apps.script" + ], + "application/vnd.ms-excel": [ + "application/vnd.google-apps.spreadsheet" + ], + "application/vnd.sun.xml.writer": [ + "application/vnd.google-apps.document" + ], + "application/vnd.ms-word.document.macroenabled.12": [ + "application/vnd.google-apps.document" + ], + "application/vnd.ms-powerpoint.slideshow.macroenabled.12": [ + "application/vnd.google-apps.presentation" + ], + "text/rtf": [ + "application/vnd.google-apps.document" + ], + "text/plain": [ + "text/plain" + ], + "application/vnd.oasis.opendocument.spreadsheet": [ + "application/vnd.google-apps.spreadsheet" + ], + "application/x-vnd.oasis.opendocument.spreadsheet": [ + "application/vnd.google-apps.spreadsheet" + ], + "image/png": [ + "image/png" + ], + "application/x-vnd.oasis.opendocument.text": [ + "application/vnd.google-apps.document" + ], + "application/msword": [ + "application/vnd.google-apps.document" + ], + "application/pdf": [ + "application/pdf" + ], + "application/json": [ + "application/json" + ], + "application/x-msmetafile": [ + "application/vnd.google-apps.drawing" + ], + "application/vnd.openxmlformats-officedocument.spreadsheetml.template": [ + "application/vnd.google-apps.spreadsheet" + ], + "application/vnd.ms-powerpoint": [ + "application/vnd.google-apps.presentation" + ], + "application/vnd.ms-excel.template.macroenabled.12": [ + "application/vnd.google-apps.spreadsheet" + ], + "image/x-bmp": [ + "image/x-bmp" + ], + "application/rtf": [ + "application/vnd.google-apps.document" + ], + "application/vnd.openxmlformats-officedocument.presentationml.template": [ + "application/vnd.google-apps.presentation" + ], + "image/x-png": [ + "image/x-png" + ], + "text/html": [ + "text/html" + ], + "application/vnd.oasis.opendocument.text": [ + "application/vnd.google-apps.document" + ], + "application/vnd.openxmlformats-officedocument.presentationml.presentation": [ + "application/vnd.google-apps.presentation" + ], + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [ + "application/vnd.google-apps.spreadsheet" + ], + "application/vnd.google-apps.script+json": [ + "application/vnd.google-apps.script" + ], + "application/vnd.openxmlformats-officedocument.presentationml.slideshow": [ + "application/vnd.google-apps.presentation" + ], + "application/vnd.ms-powerpoint.template.macroenabled.12": [ + "application/vnd.google-apps.presentation" + ], + "text/csv": [ + "text/csv" + ], + "application/vnd.oasis.opendocument.presentation": [ + "application/vnd.google-apps.presentation" + ], + "image/jpg": [ + "image/jpg" + ], + "text/richtext": [ + "application/vnd.google-apps.document" + ], + "application/postscript": [ + "application/postscript" + ], + "image/x-icon": [ + "image/x-icon" + ], + "image/tiff": [ + "image/tiff" + ], + "video/mp4": [ + "video/mp4" + ], + "application/zip": [ + "application/zip" + ], + "image/svg+xml": [ + "image/svg+xml" + ], + "application/postscript": [ + "application/postscript" + ] + }, + "exportFormats": { + "application/vnd.google-apps.form": [ + "application/zip" + ], + "application/vnd.google-apps.document": [ + "application/rtf", + "application/vnd.oasis.opendocument.text", + "text/html", + "application/pdf", + "application/epub+zip", + "application/zip", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "text/plain" + ], + "application/vnd.google-apps.drawing": [ + "image/svg+xml", + "image/png", + "application/pdf", + "image/jpeg" + ], + "application/vnd.google-apps.spreadsheet": [ + "application/x-vnd.oasis.opendocument.spreadsheet", + "text/tab-separated-values", + "application/pdf", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "text/csv", + "application/zip", + "application/vnd.oasis.opendocument.spreadsheet" + ], + "application/vnd.google-apps.script": [ + "application/vnd.google-apps.script+json" + ], + "application/vnd.google-apps.presentation": [ + "application/vnd.oasis.opendocument.presentation", + "application/pdf", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "text/plain" + ] + } + }` + + defaultformat = `{ + "application/vnd.google-apps.form": "application/zip", + "application/vnd.google-apps.document": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.google-apps.drawing": "image/png", + "application/vnd.google-apps.spreadsheet": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.google-apps.script": "application/vnd.google-apps.script+json", + "application/vnd.google-apps.presentation": "application/vnd.openxmlformats-officedocument.presentationml.presentation" + }` +) diff --git a/utl/transfer.go b/utl/transfer.go new file mode 100644 index 0000000..a90d11c --- /dev/null +++ b/utl/transfer.go @@ -0,0 +1,641 @@ +// Package utl (transfer.go) : +// These methods are for downloading, uploading and retrieving file list from or to Google Drive. +package utl + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "math" + "mime/multipart" + "net/textproto" + "net/url" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/urfave/cli" +) + +const ( + sdownloadurl = "https://script.google.com/feeds/download/export?id=" + lurl = "https://www.googleapis.com/drive/v3/files?" + driveapiurl = "https://www.googleapis.com/drive/v3/files/" + uploadurl = "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&" +) + +// FileInf : File information for downloading and uploading +type FileInf struct { + Accesstoken string `json:"-"` + DlMime string `json:"-"` + MimeType string `json:"mimeType,omitempty"` + Workdir string `json:"-"` + PstartTime time.Time `json:"-"` + WantExt string `json:"-"` + WantName string `json:"-"` + WebLink string `json:"webContentLink,omitempty"` + WebView string `json:"webViewLink,omitempty"` + SearchByName string `json:"-"` + SearchByID string `json:"-"` + FileID string `json:"id,omitempty"` + FileName string `json:"name,omitempty"` + SaveName string `json:"saved_file_name,omitempty"` + Parents []string `json:"parents,omitempty"` + UpFilename []string `json:"upload_file_name,omitempty"` + UpFileID []string `json:"uid,omitempty"` + UppedFiles []uploadedFile `json:"uploaded_files,omitempty"` + TotalEt float64 `json:"TotalElapsedTime,omitempty"` + Msgar []string `json:"message,omitempty"` +} + +// dlError : Error messages. +type dlError struct { + Error struct { + Code int `json:"code"` + Message string `json:"message"` + } `json:"error"` +} + +// project : Project structure +type project struct { + Files []struct { + ID string `json:"id,omitempty"` + Name string `json:"name"` + Type string `json:"type"` + Source string `json:"source"` + } `json:"files"` +} + +// filea : Individual file in a project +type filea struct { + ID string `json:"id,omitempty"` + Name string `json:"name"` + Type string `json:"type"` + Source string `json:"source"` +} + +// fileListSt : File list. +type fileListSt struct { + NextPageToken string `json:"nextPageToken,omitempty"` + Files []struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + MimeType string `json:"mimeType,omitempty"` + Parents []string `json:"parents,omitempty"` + CreatedTime string `json:"createdTime,omitempty"` + ModifiedTime string `json:"modifiedTime,omitempty"` + FullFileExtension string `json:"fullFileExtension,omitempty"` + Size string `json:"size,omitempty"` + WebLink string `json:"webContentLink,omitempty"` + WebView string `json:"webViewLink,omitempty"` + } +} + +// fileUploaderMeta : For uploading scripts. +type fileUploaderMeta struct { + Name string `json:"name"` + Parents []string `json:"parents,omitempty"` + MimeType string `json:"mimeType"` +} + +// uploadedFile : For uploading files. +type uploadedFile struct { + ID string `json:"id"` + Name string `json:"name"` + MimeType string `json:"mimeType"` + Parents []string `json:"parents,omitempty"` +} + +//dispDup : For duplicating values. +type dispDup struct { + Name string + FileID string + MimeType string + ModifiedTime string +} + +// saveScript : Back up a project. +func (p *FileInf) saveScript(data []byte, c *cli.Context) *FileInf { + var f project + json.Unmarshal(data, &f) + if c.Bool("rawdata") { + filename := filepath.Join(p.Workdir, p.FileName+".json") + p.SaveName = p.FileName + ".json" + p.Msgar = append(p.Msgar, fmt.Sprintf("Saved project as a JSON file '%s.json'.", p.FileName)) + btok, _ := json.MarshalIndent(f, "", "\t") + ioutil.WriteFile(filename, btok, 0777) + } else { + p.SaveName = "" + if len(f.Files) == 1 { + p.Msgar = append(p.Msgar, fmt.Sprintf("%s has %d script.", p.FileName, len(f.Files))) + } else { + p.Msgar = append(p.Msgar, fmt.Sprintf("%s has %d scripts.", p.FileName, len(f.Files))) + } + for _, e := range f.Files { + saveName := p.FileName + "_" + e.Name + "." + func(ex string) string { + var eext string + if len(ex) > 0 { + eext = ex + } else { + eext = "gs" + } + return eext + }(p.WantExt) + src := fmt.Sprintf("// Script ID in Project = %s \n%s", e.ID, e.Source) + ioutil.WriteFile(filepath.Join(p.Workdir, saveName), []byte(src), 0777) + p.Msgar = append(p.Msgar, fmt.Sprintf("Script was downloaded as '%s'.", saveName)) + } + } + return p +} + +// Downloader : Download files. +func (p *FileInf) Downloader(c *cli.Context) *FileInf { + ext := strings.ToLower(p.WantExt) + if len(ext) > 0 { + p.DlMime = extToMime(ext) + } else { + p.DlMime, ext = defFormat(p.MimeType) + } + if len(p.FileID) > 0 { + var body []byte + var gm map[string]interface{} + json.Unmarshal([]byte(googlemimetypes), &gm) + if gm["exportFormats"].(map[string]interface{})[p.MimeType] != nil { + for _, e := range gm["exportFormats"].(map[string]interface{})[p.MimeType].([]interface{}) { + if e == p.DlMime { + p.SaveName = p.FileName + "." + ext + } + } + if len(p.SaveName) == 0 { + dispRes, _ := json.MarshalIndent(gm["exportFormats"], "", " ") + fmt.Fprintf(os.Stderr, "Error: Bad extension or No extension. It supports as follows.\n%s ", string(dispRes)) + os.Exit(1) + } + if p.MimeType == "application/vnd.google-apps.script" { + p, body = p.writeFile(sdownloadurl + p.FileID + "&format=json") + p.saveScript(body, c) + } else { + p, _ = p.writeFile(driveapiurl + p.FileID + "/export?mimeType=" + p.DlMime) + } + } else { + p.SaveName = p.FileName + p, _ = p.writeFile(driveapiurl + p.FileID + "?alt=media") + } + } else { + fmt.Fprintf(os.Stderr, "Error: Please input File Name or File ID. ") + os.Exit(1) + } + p.TotalEt = math.Trunc(time.Now().Sub(p.PstartTime).Seconds()*1000) / 1000 + return p +} + +// writeFile : Create files on local. +func (p *FileInf) writeFile(durl string) (*FileInf, []byte) { + r := &RequestParams{ + Method: "GET", + APIURL: durl, + Data: nil, + Contenttype: "application/x-www-form-urlencoded", + Accesstoken: p.Accesstoken, + Dtime: 10, + } + body, err := r.FetchAPI() + var er dlError + json.Unmarshal(body, &er) + if err != nil || er.Error.Code-300 >= 0 { + fmt.Print(fmt.Sprintf("Error: %s Status code is %d. FileID: %s ", er.Error.Message, er.Error.Code, p.FileID)) + os.Exit(1) + } + if p.MimeType != "application/vnd.google-apps.script" { + ioutil.WriteFile(filepath.Join(p.Workdir, p.SaveName), body, 0777) + p.Msgar = append(p.Msgar, fmt.Sprintf("File was downloaded as '%s'.", p.SaveName)) + } + return p, body +} + +// nameToID : +func (p *FileInf) nameToID(name string) ([]byte, error) { + number := 1000 + tokenparams := url.Values{} + tokenparams.Set("orderBy", "name") + tokenparams.Set("pageSize", strconv.Itoa(number)) + tokenparams.Set("q", "name='"+name+"' and trashed=false") + tokenparams.Set("fields", "files(createdTime,fullFileExtension,id,mimeType,modifiedTime,name,parents,size,webContentLink,webViewLink)") + r := &RequestParams{ + Method: "GET", + APIURL: lurl + tokenparams.Encode(), + Data: nil, + Contenttype: "application/x-www-form-urlencoded", + Accesstoken: p.Accesstoken, + Dtime: 30, + } + return r.FetchAPI() +} + +// idToName : Convert file ID to file name. +func (p *FileInf) idToName(id string) ([]byte, error) { + tokenparams := url.Values{} + tokenparams.Set("fields", "createdTime,fullFileExtension,id,mimeType,modifiedTime,name,parents,size,webContentLink,webViewLink") + r := &RequestParams{ + Method: "GET", + APIURL: driveapiurl + id + "?" + tokenparams.Encode(), + Data: nil, + Contenttype: "application/x-www-form-urlencoded", + Accesstoken: p.Accesstoken, + Dtime: 30, + } + return r.FetchAPI() +} + +// GetFileinf : Retrieve file infomation using Drive API. +func (p *FileInf) GetFileinf() *FileInf { + if len(p.FileID) > 0 { + body, err := p.idToName(p.FileID) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: File ID '%s' Not found. %v .", p.FileID, err) + os.Exit(1) + } + var er dlError + json.Unmarshal(body, &er) + if err != nil || er.Error.Code-300 >= 0 { + fmt.Fprintf(os.Stderr, fmt.Sprintf("Error: %s Status code is %d. ", er.Error.Message, er.Error.Code)) + os.Exit(1) + } + json.Unmarshal(body, &p) + } else if len(p.WantName) > 0 { + finf, err := p.nameToID(p.WantName) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v. ", err) + os.Exit(1) + } + var fl fileListSt + json.Unmarshal(finf, &fl) + if len(fl.Files) == 1 { + p.FileID = fl.Files[0].ID + p.FileName = fl.Files[0].Name + p.MimeType = fl.Files[0].MimeType + p.WebLink = fl.Files[0].WebLink + p.WebView = fl.Files[0].WebView + } else if len(fl.Files) > 1 { + fmt.Printf("# %d files were found. Please download them using File ID.\n", len(fl.Files)) + for i := range fl.Files { + dd := &dispDup{ + Name: fl.Files[i].Name, + FileID: fl.Files[i].ID, + MimeType: fl.Files[i].MimeType, + ModifiedTime: fl.Files[i].ModifiedTime, + } + rd, _ := json.MarshalIndent(dd, "", " ") + fmt.Printf("%s\n", rd) + } + os.Exit(1) + } else { + fmt.Fprintf(os.Stderr, "Error: File name '%s' is not found. ", p.WantName) + os.Exit(1) + } + } + if p.MimeType == "application/vnd.google-apps.folder" { + fmt.Fprintf(os.Stderr, "Error: '%s' is a Folder. Cannot download Folder yet. ", p.FileID) + os.Exit(1) + } + return p +} + +// extToMime : Convert from extension to mimeType of the file on Local. +func extToMime(ext string) string { + var fm map[string]interface{} + json.Unmarshal([]byte(extVsmime), &fm) + st, _ := fm[strings.Replace(strings.ToLower(ext), ".", "", 1)].(string) + return st +} + +// defFormat : Default download format +func defFormat(mime string) (string, string) { + var df map[string]interface{} + json.Unmarshal([]byte(defaultformat), &df) + dmime, _ := df[mime].(string) + var ext string + var fm map[string]interface{} + json.Unmarshal([]byte(extVsmime), &fm) + for i, v := range fm { + if v == dmime { + ext = i + } + } + return dmime, ext +} + +// scriptUploader : For uploading scripts. +func (p *FileInf) scriptUploader(metadata map[string]interface{}, pr []byte) *FileInf { + tokenparams := url.Values{} + tokenparams.Set("fields", "id,mimeType,name,parents") + var b bytes.Buffer + w := multipart.NewWriter(&b) + part := make(textproto.MIMEHeader) + part.Set("Content-Type", "application/json") + data, err := w.CreatePart(part) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v. ", err) + os.Exit(1) + } + re, _ := json.Marshal(metadata) + if _, err = io.Copy(data, bytes.NewReader(re)); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v. ", err) + os.Exit(1) + } + data, err = w.CreatePart(part) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v. ", err) + os.Exit(1) + } + if _, err = io.Copy(data, bytes.NewReader(pr)); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v. ", err) + os.Exit(1) + } + w.Close() + r := &RequestParams{ + Method: "POST", + APIURL: uploadurl + tokenparams.Encode(), + Data: &b, + Contenttype: w.FormDataContentType(), + Accesstoken: p.Accesstoken, + Dtime: 10, + } + body, err := r.FetchAPI() + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v. ", err) + os.Exit(1) + } + var uf uploadedFile + json.Unmarshal(body, &uf) + p.UppedFiles = append(p.UppedFiles, uf) + return p +} + +// fileUploader : For uploading files. +func (p *FileInf) fileUploader(metadata map[string]interface{}, file string) *FileInf { + tokenparams := url.Values{} + tokenparams.Set("fields", "id,mimeType,name,parents") + var b bytes.Buffer + w := multipart.NewWriter(&b) + part := make(textproto.MIMEHeader) + part.Set("Content-Type", "application/json") + data, err := w.CreatePart(part) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v. ", err) + os.Exit(1) + } + re, _ := json.Marshal(metadata) + if _, err = io.Copy(data, bytes.NewReader(re)); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v. ", err) + os.Exit(1) + } + fs, err := os.Open(file) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v. ", err) + os.Exit(1) + } + defer fs.Close() + data, err = w.CreateFormFile("file", file) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v. ", err) + os.Exit(1) + } + if _, err = io.Copy(data, fs); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v. ", err) + os.Exit(1) + } + w.Close() + r := &RequestParams{ + Method: "POST", + APIURL: uploadurl + tokenparams.Encode(), + Data: &b, + Contenttype: w.FormDataContentType(), + Accesstoken: p.Accesstoken, + Dtime: 10, + } + body, err := r.FetchAPI() + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v. ", err) + os.Exit(1) + } + var uf uploadedFile + json.Unmarshal(body, &uf) + p.UppedFiles = append(p.UppedFiles, uf) + return p +} + +// Uploader : Main method for uploading +// "$ ggsrun u -f t1.gs,t2.gs" or "$ ggsrun u -f "t1.gs, t2.gs"" +func (p *FileInf) Uploader(c *cli.Context) *FileInf { + if len(c.String("projectname")) == 0 { + for _, elm := range p.UpFilename { + metadata := &fileUploaderMeta{ + Name: filepath.Base(elm), + Parents: []string{c.String("parentfolderid")}, + MimeType: func(flag bool) string { + var r string + if !flag { + r = extToGMime(filepath.Ext(elm)) + } + return r + }(c.Bool("noconvert")), + } + if metadata.MimeType == "application/vnd.google-apps.script" { + var pr project + filedata := &filea{ + Name: metadata.Name, + Type: "server_js", + Source: ConvGasToUpload(elm), + } + pr.Files = append(pr.Files, *filedata) + pre, _ := json.Marshal(pr) + upmeta, _ := json.Marshal(metadata) + var u map[string]interface{} + json.Unmarshal(upmeta, &u) + if len(c.String("parentfolderid")) == 0 { + delete(u, "parents") + } + _ = p.scriptUploader(u, pre) + p.Msgar = append(p.Msgar, fmt.Sprintf("Uploaded %s as %s. ", filepath.Base(elm), metadata.Name)) + } else { + upmeta, _ := json.Marshal(metadata) + var u map[string]interface{} + json.Unmarshal(upmeta, &u) + if len(c.String("parentfolderid")) == 0 { + delete(u, "parents") + } + p.fileUploader(u, elm) + p.Msgar = append(p.Msgar, fmt.Sprintf("Uploaded %s as %s.", filepath.Base(elm), metadata.Name)) + } + } + } else { + metadata := &fileUploaderMeta{ + Name: c.String("projectname"), + Parents: []string{c.String("parentfolderid")}, + MimeType: "application/vnd.google-apps.script", + } + upmeta, _ := json.Marshal(metadata) + var u map[string]interface{} + json.Unmarshal(upmeta, &u) + if len(c.String("parentfolderid")) == 0 { + delete(u, "parents") + } + var pr project + for _, elm := range p.UpFilename { + if filepath.Ext(elm) == ".gs" || + filepath.Ext(elm) == ".gas" || + filepath.Ext(elm) == ".js" || + filepath.Ext(elm) == ".htm" || + filepath.Ext(elm) == ".html" { + filedata := &filea{ + Name: strings.Replace(filepath.Base(elm), filepath.Ext(elm), "", -1), + Type: func(ex string) string { + var scripttype string + switch ex { + case ".gs", ".gas", ".js": + scripttype = "server_js" + case ".htm", ".html": + scripttype = "html" + } + return scripttype + }(filepath.Ext(elm)), + Source: ConvGasToUpload(elm), + } + pr.Files = append(pr.Files, *filedata) + } + } + p.Msgar = append(p.Msgar, fmt.Sprintf("Uploaded %d scripts as a project with a name of '%s'.", len(p.UpFilename), metadata.Name)) + pre, _ := json.Marshal(pr) + _ = p.scriptUploader(u, pre) + } + return p +} + +// extToGMime : Convert from extension to mimeType of the files on Google. +func extToGMime(ext string) string { + var fm map[string]interface{} + json.Unmarshal([]byte(extVsmime), &fm) + st, _ := fm[strings.Replace(strings.ToLower(ext), ".", "", 1)].(string) + if len(st) == 0 { + fmt.Fprintf(os.Stderr, "Error: Extension of '%s' cannot be uploaded. ", ext) + os.Exit(1) + } + var gm map[string]interface{} + json.Unmarshal([]byte(googlemimetypes), &gm) + return gm["importFormats"].(map[string]interface{})[st].([]interface{})[0].(string) +} + +// GetFileList : Retrieving file list on Google Drive. +func (p *FileInf) GetFileList(c *cli.Context) *FileInf { + if len(c.String("searchbyname")) > 0 { + p.SearchByName = c.String("searchbyname") + body, err := p.nameToID(p.SearchByName) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v. ", err) + os.Exit(1) + } + var fl fileListSt + json.Unmarshal(body, &fl) + if len(fl.Files) == 1 { + p.FileID = fl.Files[0].ID + p.FileName = fl.Files[0].Name + p.MimeType = fl.Files[0].MimeType + p.Parents = fl.Files[0].Parents + p.WebView = fl.Files[0].WebView + } else if len(fl.Files) > 1 { + for i := range fl.Files { + fmt.Printf("{\n Name: \"%s\",\n ID: \"%s\",\n ModifiedTime: \"%s\",\n URL: \"%s\"\n}\n", fl.Files[i].Name, fl.Files[i].ID, fl.Files[i].ModifiedTime, fl.Files[i].WebView) + } + os.Exit(1) + } else { + fmt.Fprintf(os.Stderr, "Error: File name '%s' is not found. ", p.SearchByName) + os.Exit(1) + } + return p + } + if len(c.String("searchbyid")) > 0 { + p.SearchByID = c.String("searchbyid") + body, err := p.idToName(p.SearchByID) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: File ID '%s' is not found. ", p.SearchByID) + os.Exit(1) + } + json.Unmarshal(body, &p) + return p + } + var fm fileListSt + var fl fileListSt + var dmy fileListSt + fm.NextPageToken = "" + for i := 0; ; { + _ = i + body, err := p.getList(fm.NextPageToken) + json.Unmarshal(body, &fl) + fm.NextPageToken = fl.NextPageToken + fm.Files = append(fm.Files, fl.Files...) + fl.NextPageToken = "" + fl.Files = dmy.Files + if len(fm.NextPageToken) == 0 || err != nil { + break + } + } + var fol, fil []string + for i := range fm.Files { + if strings.Contains(fm.Files[i].MimeType, "folder") { + fol = append(fol, fm.Files[i].Name) + } else { + fil = append(fil, fm.Files[i].Name) + } + } + p.Msgar = append(p.Msgar, fmt.Sprintf("Total: %d, File: %d, Folder: %d", len(fm.Files), len(fil), len(fol))) + p.Msgar = append(p.Msgar, fmt.Sprintf("If you want a file list, please use option '-s' or '-f'. The file name is automatically given.")) + if c.Bool("stdout") { + fmt.Printf("| File name | File ID | Modified time | Create time | Type |\n") + var ftype string + for i := range fm.Files { + if strings.Contains(fm.Files[i].MimeType, "folder") { + ftype = "Folder" + } else { + ftype = "File" + } + fmine := fmt.Sprintf("| %s | %s | %s | %s | %s |\n", fm.Files[i].Name, fm.Files[i].ID, fm.Files[i].ModifiedTime, fm.Files[i].CreatedTime, ftype) + fmt.Print(fmine) + } + } + if c.Bool("file") { + filename := filepath.Join(p.Workdir, p.PstartTime.Format("Files_20060102_150405")+".json") + p.Msgar = append(p.Msgar, fmt.Sprintf("Saved a JSON file as %s.", filename)) + btok, _ := json.MarshalIndent(fm, "", "\t") + ioutil.WriteFile(filename, btok, 0777) + } + return p +} + +// getList : For retrieving file list. +func (p *FileInf) getList(ptoken string) ([]byte, error) { + number := 1000 + tokenparams := url.Values{} + tokenparams.Set("orderBy", "name") + tokenparams.Set("pageSize", strconv.Itoa(number)) + tokenparams.Set("q", "trashed=false") + tokenparams.Set("fields", "files(createdTime,fullFileExtension,id,mimeType,modifiedTime,name,parents,size),nextPageToken") + if len(ptoken) > 0 { + tokenparams.Set("pageToken", ptoken) + } + r := &RequestParams{ + Method: "GET", + APIURL: lurl + tokenparams.Encode(), + Data: nil, + Contenttype: "application/x-www-form-urlencoded", + Accesstoken: p.Accesstoken, + Dtime: 30, + } + body, err := r.FetchAPI() + return body, err +}