diff --git a/README.md b/README.md index 1accf6c..752ad28 100644 --- a/README.md +++ b/README.md @@ -19,15 +19,17 @@ Will you want to develop GAS on your local PC? Generally, when we develop GAS, w Features of "ggsrun" are as follows. -1. **[Develops GAS using your terminal and text editor which got accustomed to using.](help/README.md#demoterminal)** +1. **[Develops GAS using your local terminal and text editor which got accustomed to using.](help/README.md#demoterminal)**Updated! (v1.4.0) 1. **[Executes GAS by giving values to your script.](help/README.md#givevalues)** 1. **[Executes GAS made of CoffeeScript.](help/README.md#coffee)** 1. **[Downloads spreadsheet, document and presentation, while executes GAS, simultaneously.](help/README.md#filedownload)** -1. **[Creates, updates and backs up project with GAS.](help/README.md#fileupdate)** 1. **[Downloads files from Google Drive and Uploads files to Google Drive.](help/README.md#fileupdown)** -1. **[Downloads standalone script and bound script.](help/README.md#DownloadBoundScript)** -1. **[Rearranges scripts in project.](help/README.md#rearrangescripts)** NEW! (v1.3.2) -1. **[Modifies Manifests in project.](help/README.md#ModifyManifests)** NEW! (v1.3.3) +1. **[Downloads standalone script and bound script.](help/README.md#DownloadFiles)** Updated! (v1.4.0) +1. **[Upload script files and create project as standalone script and container-bound script.](help/README.md#UploadFiles)** Updated! (v1.4.0) +1. **[Update project.](help/README.md#Update_Project)** Updated! (v1.4.0) +1. **[Retrieve revision files of Google Docs and retrieve versions of projects.](help/README.md#RevisionFile)** Updated! (v1.4.0) +1. **[Rearranges scripts in project.](help/README.md#rearrangescripts)** Updated! (v1.4.0) +1. **[Modifies Manifests in project.](help/README.md#ModifyManifests)** # How to Install @@ -42,18 +44,32 @@ Use go get. $ go get -u github.com/tanaikech/ggsrun ~~~ + ## 2. Basic setting flow When you click each link of title, you can see the detail information. 1. [Setup ggsrun Server (at Google side)](help/README.md#Setup_ggsrun_Server) - Create new project and install the server as a library. - - Script ID of the library is "**``115-19njNHlbT-NI0hMPDnVO1sdrw2tJKCAJgOTIAPbi_jq3tOo4lVRov``**". - - **After installed the library, please push the save button at the script editor.** This is important! By this, the library is completely reflected. -1. [Install Google Apps Script API(Execution API)](help/README.md#Install_Execution_API) - - For the created project, deploy API executable. - - Enable **[Google Apps Script API(Execution API)](https://console.cloud.google.com/apis/library/script.googleapis.com/)** and **[Drive API](https://console.cloud.google.com/apis/api/drive.googleapis.com/)** at API console. + - [Deploy API executable](https://developers.google.com/apps-script/api/how-tos/execute#step_1_deploy_the_script_as_an_api_executable). Choose "Only myself" as "Who has access to the script" + - [Install the server as a library.](https://developers.google.com/apps-script/guides/libraries#managing_libraries) Script ID of the library is "**``115-19njNHlbT-NI0hMPDnVO1sdrw2tJKCAJgOTIAPbi_jq3tOo4lVRov``**". + - **After installed the library, please push the save button at the script editor.** This is very important! By this, the library is completely reflected. 1. [Get Client ID, Client Secret](help/README.md#GetClientID) - - Create a credential as **Other** and download **``client_secret.json``**. + - On the Script Editor + - Resources -> Cloud Platform Project + - Click the lower part of "This script is currently associated with project:" + - In "Getting Started", Click "Enable APIs and get credentials like keys". + - On "API APIs&services" + - Click "Credentials" at left side. + - At "Create Credentials", Click OAuth client ID. + - Choose **Other** + - 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. +1. [Enable APIs](help/README.md#Install_Execution_API) + - ggsrun uses Google Apps Script API and Drive API. Please enable them at API console. You can directly access them as follows. Project ID can be seen at downloaded ``client_secret.json``. + - ``https://console.cloud.google.com/apis/library/script.googleapis.com/?project=### project ID ###`` + - **Also here [https://script.google.com/home/usersettings](https://script.google.com/home/usersettings) has to be enabled. Please turn ON.** + - ``https://console.cloud.google.com/apis/api/drive.googleapis.com/?project=### project ID ###`` 1. [Create configure file for ggsrun](help/README.md#Createconfigurefile) - Run ``$ ggsrun auth`` at the directory with ``client_secret.json``. 1. [Test Run](help/README.md#Runggsrun) @@ -62,6 +78,19 @@ When you click each link of title, you can see the detail information. Congratulation! You got ggsrun! + +# To users which are using ggsrun with v1.3.4 and/or less Updated! (v1.4.0) +Please reauthorize to include a new scope to the access token as follows. + +1. Confirm whether Google Apps Script API is enabled. You can directly access it as follows. Project ID can be seen at the downloaded ``client_secret.json``. + - ``https://console.cloud.google.com/apis/library/script.googleapis.com/?project=### project ID ###`` + - Also here [https://script.google.com/home/usersettings](https://script.google.com/home/usersettings) has to be enabled. Please turn ON. +1. Add a scope of ``https://www.googleapis.com/auth/script.projects`` to ``ggsrun.cfg``. +1. Run the following command under the directory with ``client_secret.json`` and ``ggsrun.cfg``. + - ``$ ggsrun auth`` + +Completed! + # How to use ggsrun 1. [Executes GAS and Retrieves Result Values](help/README.md#ExecutesGASandRetrievesResultValues) 1. [Executes GAS with Values and Retrieves Feedbacked Values](help/README.md#ExecutesGASwithValuesandRetrievesFeedbackedValues) @@ -69,15 +98,13 @@ Congratulation! You got ggsrun! 1. [Executes GAS with Values and Downloads File](help/README.md#ExecutesGASwithValuesandDownloadsFile) 1. [Executes Existing Functions on Project](help/README.md#ExecutesExistingFunctionsonProject) 1. [Download Files](help/README.md#DownloadFiles) - 1. [How to Download Container-Bound Scripts](help/README.md#DownloadBoundScript) NEW! (v1.3.4) - 1. [Upload Files](help/README.md#UploadFiles) 1. [Show File List](help/README.md#ShowFileList) 1. [Search Files](help/README.md#SearchFiles) 1. [Update Project](help/README.md#Update_Project) -1. [Retrieve Revision Files](help/README.md#RevisionFile) -1. [Rearrange Script in Project](help/README.md#rearrangescripts) NEW! (v1.3.2) -1. [Modify Manifests](help/README.md#ModifyManifests) NEW! (v1.3.3) +1. [Retrieve Revision Files and Versions of Projects](help/README.md#RevisionFile) +1. [Rearrange Script in Project](help/README.md#rearrangescripts) +1. [Modify Manifests](help/README.md#ModifyManifests) # Applications 1. [For Sublime Text](help/README.md#demosublime) diff --git a/doc.go b/doc.go index 9f0faa1..23eb467 100644 --- a/doc.go +++ b/doc.go @@ -14,15 +14,24 @@ Will you want to develop GAS on your local PC? Generally, when we develop GAS, w 4. Downloads spreadsheet, document and presentation, while executes GAS, simultaneously. -5. Creates, updates and backs up project with GAS. +5. Upload files to Google Drive. When files are uploaded, also they can be converted by options. -6. Downloads files from Google Drive and Uploads files to Google Drive. Also container-bound scripts can be downloaded. +6. Creates, updates and backs up project of both standalone type and bound script type. -7. Download revision files from Google Drive. +7. Creates Google Docs (Spreadsheet, Document, Slide and Form) and create bound script in the created Google Docs. -8. Rearranges scripts in project. +8. Downloads files from Google Drive and Uploads files to Google Drive. Also container-bound scripts can be downloaded. + +9. Downloads revision files from Google Drive. + +10. Rearranges files in project of both standalone type and bound script type. + +11. Modifies Manifests (appsscript.json) in project. + +12. Remove files in the project of both standalone type and bound script type. + +13. Retrieve revision file list and revision data. -9. Modifies Manifests (appsscript.json) in project. You can see the release page https://github.com/tanaikech/ggsrun/releases diff --git a/ggsrun.go b/ggsrun.go index 2cd3d49..511d321 100644 --- a/ggsrun.go +++ b/ggsrun.go @@ -15,7 +15,7 @@ func main() { 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.3.4" + app.Version = "1.4.0" app.Commands = []cli.Command{ { Name: "exe1", @@ -155,14 +155,14 @@ func main() { Name: "filename, f", Usage: "File Name on Google Drive", }, - cli.StringFlag{ - Name: "projectid, pi", - Usage: "Project ID of 'bound scripts' of Google Sheets, Docs, or Forms file. Please use this for downloading 'bound scripts'.", - }, - cli.StringFlag{ - Name: "boundscriptname, bn", - Usage: "This is used for the option of 'projectid'. Using this option, when you download the 'bound scripts', you can give the filename.", - }, + // cli.StringFlag{ + // Name: "projectid, pi", + // Usage: "Project ID of 'bound scripts' of Google Sheets, Docs, or Forms file. Please use this for downloading 'bound scripts'.", + // }, + // cli.StringFlag{ + // Name: "boundscriptname, bn", + // Usage: "This is used for the option of 'projectid'. Using this option, when you download the 'bound scripts', you can give the filename.", + // }, cli.StringFlag{ Name: "extension, e", Usage: "Extension (File format of downloaded file)", @@ -171,6 +171,10 @@ func main() { Name: "rawdata, r", Usage: "Save a project with GAS scripts as raw data (JSON data).", }, + cli.StringFlag{ + Name: "deletefile", + Usage: "Value is file ID. This can delete a file using a file ID on Google Drive.", + }, cli.BoolFlag{ Name: "jsonparser, j", Usage: "Display results by JSON parser", @@ -186,16 +190,33 @@ func main() { Flags: []cli.Flag{ cli.StringFlag{ Name: "filename, f", - Usage: "File Name on local PC", + Usage: "File Name on local PC. Please input files you want to upload.", }, cli.StringFlag{ Name: "parentfolderid, p", Usage: "Folder ID of parent folder on Google Drive", }, + cli.StringFlag{ + Name: "parentid, pid", + Usage: "File ID of Google Docs (Spreadsheet, Document, Slide, Form) for creating container bound-script.", + }, + cli.StringFlag{ + Name: "timezone, tz", + Usage: "Time zone of project. Please use this together with creating new project. When new project is created by API, time zone doesn't become the local time zone. (This might be a bug.) So please input this.", + }, cli.StringFlag{ Name: "projectname, pn", Usage: "Upload several GAS scripts as a project.", }, + cli.StringFlag{ + Name: "googledocname, gn", + Usage: "Filename of Google Docs which is created.", + }, + cli.StringFlag{ + Name: "projecttype, pt", + Usage: "You can select where it creates a new project. Please input 'spreadsheet', 'document', 'slide' and 'form'. When you select one of them, new project is created as a bound script. If this option is not used, new project is created as a standalone script. This is a default.", + Value: "standalone", + }, cli.BoolFlag{ Name: "noconvert, nc", Usage: "If you don't want to convert file to Google Apps format.", @@ -215,7 +236,11 @@ func main() { Flags: []cli.Flag{ cli.StringFlag{ Name: "filename, f", - Usage: "File name. It's source files for updating.", + Usage: "File name. It's source files for updating. When you set files which are not in the project, the files are added to the project. When you set files which are in the project, the files are overwritten to the files with same filename.", + }, + cli.BoolFlag{ + Name: "deletefiles", + Usage: "When you use this bool flag, projectid and filename, they are removed from the project.", }, cli.StringFlag{ Name: "projectid, p", @@ -254,10 +279,18 @@ func main() { Name: "download, d", Usage: "Value is revision ID. Download revision file using it and file ID.", }, + cli.StringFlag{ + Name: "createversion, cv", + Usage: "Create new version of GAS project. Please input the description of version as string.", + }, 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", diff --git a/help/README.md b/help/README.md index 0abc497..2687779 100644 --- a/help/README.md +++ b/help/README.md @@ -29,7 +29,7 @@ ggsrun - Show File List - Search Files - Update Project - - Retrieve Revision Files + - Retrieve Revision Files and Versions of Projects - Rearrange Script in Project - Modify Manifests - [Applications](#Applications) @@ -57,19 +57,28 @@ Will you want to develop GAS on your local PC? Generally, when we develop GAS, w Features of "ggsrun" are as follows. -1. **[Develops GAS using your terminal and text editor which got accustomed to using.](#demoterminal)** +1. **[Develops GAS using your terminal and text editor which got accustomed to using.](#demosublime)** -2. **[Executes GAS by giving values to your script.](#givevalues)** +1. **[Executes GAS by giving values to your script.](#ExecutesGASandRetrievesResultValues)** -3. **[Executes GAS made of CoffeeScript.](#coffee)** +1. **[Executes GAS made of CoffeeScript.](#CoffeeScript)** -4. **[Downloads spreadsheet, document and presentation, while executes GAS, simultaneously.](#filedownload)** +1. **[Downloads spreadsheet, document and presentation, while executes GAS, simultaneously.](#ExecutesGASwithValuesandDownloadsFile)** -5. **[Creates, updates and backs up project with GAS.](#fileupdate)** +1. **[Downloads files from Google Drive and Uploads files to Google Drive.](#DownloadFiles)** -6. **[Downloads files from Google Drive and Uploads files to Google Drive.](#fileupdown)** +1. **[Downloads standalone script and bound script.](#DownloadFiles)** + +1. **[Upload script files and create project as standalone script and container-bound script.](#UploadFiles)** + +1. **[Update project.](#Update_Project)** + +1. **[Retrieve revision files of Google Docs and retrieve versions of projects.](#RevisionFile)** + +1. **[Rearranges scripts in project.](#rearrangescripts)** + +1. **[Modifies Manifests in project.](#ModifyManifests)** -7. **[Rearranges scripts in project.](#rearrangescripts)** You can see the release page [here](https://github.com/tanaikech/ggsrun/releases). @@ -105,7 +114,7 @@ function main(range) { # Google APIs ggsrun uses Google Apps Script API(Execution API), Web Apps and Drive API on Google. -1. **[Google Apps Script API(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). +1. **[Google Apps Script API(previously known as 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). The reference of Google Apps Script API is [here](https://developers.google.com/apps-script/api/reference/rest/). 2. **[Drive API](https://developers.google.com/drive/v3/web/about-sdk)** : This can be used for downloading and uploading files. @@ -118,7 +127,7 @@ 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 | +| 1 | exe1
(e1) | Execution API | High | Slow\*1 | Yes | No | No | Upload
& Save to project
| Standalone and Container-bound Script NEW! | 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 | @@ -239,7 +248,7 @@ By installing this, you can use command ``exe1`` and ``exe2``. To use command `` - -> Click "Close" -#### 2. Enable APIs (Google Apps Script API(Execution API) and Drive API) +#### 2. Enable APIs (Google Apps Script API and Drive API) 1. On the Script Editor - -> Resources - -> Cloud Platform Project @@ -247,13 +256,14 @@ By installing this, you can use command ``exe1`` and ``exe2``. To use command `` 2. On "API APIs&services" - In "Getting Started", Click "Enable APIs and get credentials like keys". - Click Library at left side. - - -> At "Search APIs and services", Input "**Google Apps Script API**", Click it. - - -> **Enable "Google Apps Script API(Execution API)"** - -> You can enable it at [this URL](https://console.cloud.google.com/apis/library/script.googleapis.com/). + - -> At "Search APIs and services", Input "**apps script**", Click it. + - -> **Enable "Google Apps Script API"** + - -> You can enable it at [this URL](https://console.cloud.google.com/apis/library/script.googleapis.com/). + - **Also here [https://script.google.com/home/usersettings](https://script.google.com/home/usersettings) has to be enabled. Please turn ON.** - Back to "API Library". - -> At "Search APIs and services", Input "**drive api**", Click it. - -> **Enable "Google Drive API"** - -> You can enable it at [this URL](https://console.cloud.google.com/apis/api/drive.googleapis.com/). + - -> You can enable it at [this URL](https://console.cloud.google.com/apis/api/drive.googleapis.com/). #### 3. Get Client ID, Client Secret @@ -782,6 +792,8 @@ When a result with error of "exe1" is below. You can see various error messages. **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.** +> Recently, users can use [Stackdriver](https://developers.google.com/apps-script/guides/logging). You can also use for this situation. + ## 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``. @@ -878,9 +890,11 @@ $ 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. - -### How to Download Container-Bound Scripts -Here, I could notice that the container-bound scripts can be downloaded! From version 1.3.0, the container-bound scripts could be downloaded. +When you download project files which are standalone script and container-bound script, you can use the option of ``--raw``. You can download raw files of project by this. + +~~~bash +$ ggsrun d -i fileId -r +~~~ - In order to download container-bound scripts, the project ID of container-bound scripts is required. The project ID can be retrieved as follows. - Open the project. And please operate follows using click. @@ -888,41 +902,14 @@ You can convert only from Google Docs Files (spreadsheet, slide, documentation a - -> Project properties - -> Get Script ID (**This is the project ID.**) -You can download the project by the following command. - -~~~bash -$ ggsrun d -pi project_id -bn filename -~~~ - -- pi: project_id -- bn: filename which is used for downloading. - -**Limitation :** +> For the container-bound script, please download by file ID. Because for the baound script, Google APIs doesn't privide for retrieving file ID from filename yet. -- The file information of container-bound scripts cannot be retrieved by Drive API. So the filename cannot be retrieved from the project ID. - - When the option ``bn`` is not used, the prefix of filename of the downloaded project is the project ID. - - When the option ``bn`` is used, the prefix of filename of the downloaded project is the filename you gave. +**Delete files** -### 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. +You can also delete files using file ID. -OPTIONS: - --fileid value, -i value File ID on Google Drive. Using file ID, you can download all files except for bound scripts. - --filename value, -f value File Name on Google Drive - --projectid value, --pi value Project ID of 'bound scripts' of Google Sheets, Docs, or Forms file. Please use this for downloading 'bound scripts'. - --boundscriptname value, --bn value This is used for the option of 'projectid'. Using this option, when you download the 'bound scripts', you can give the filename. - --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 +~~~bash +$ ggsrun d --deletefile [fileId] ~~~ @@ -939,20 +926,42 @@ At this time, you can upload files to the specific folder using option ``-p [par **Run :** Uploads scripts -This is not the update. This is uploaded a new file to Google Drive. +This is not the update. This is uploaded as a new file to Google Drive. ~~~bash -$ ggsrun u -f [script filename .gs, .gas, .js] +$ ggsrun u -f [script filename .gs, .gas, .js] -tz [timezone] ~~~ Files upload and convert to GAS. If you want to create a project using several scripts and HTMLs, please use as follows. +> **"timezone" :** For example, my timeZone is "Asia/Tokyo". So I use ``-tz "Asia/Tokyo"``. Please input it for yourself. If you don't use this option, it is automatically defined by Google side. But this might not be your timeZone. Don't worry. You can modify even after created. + ~~~bash -$ ggsrun u --pn [project name] -f script1.gs,script2.gs,index.html +$ ggsrun u -pn [project name] -f script1.gs,script2.gs,index.html -tz [timezone] ~~~ 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. + +Also you can upload script files as the container-bound script of new Spreadsheet. + +~~~bash +$ ggsrun u -pn [project name] -f [script files] -pt [spreadsheet, document, slide, form] -tz [timezone] +~~~ + +For example, when you want to upload script files as the container-bound script of new spreadsheet. + +~~~bash +$ ggsrun u -pn samplename -f script1.gs,script2.gs,index.html -pt spreadsheet -tz "Asia/Tokyo" +~~~ + +When this sample command runs, a new Spreadsheet is created and the script files are uploaded as a container-bound script. "samplename" is used for spreadsheet and project name. + +> **IMPORTANT :** + +> 1. After Manifests was added to GAS, the time zone can be set by it. But when a new project is created by API, I noticed that the time zone is different from own local time zone. When a new project is manually created by browser, the time zone is the same to own local time zone. I think that this may be a bug. So I added an option for setting time zone when a new project is created. And also I reported about this to [Google Issue Tracker](https://issuetracker.google.com/issues/72019223). +1. If you want to create a bound script in Slide, an error occurs. When a bound script can be created to Apreadsheet, Document and Form using Apps Script API. Furthermore, when the bound script in Slide is updated, it works fine. So I think that this may be also a bug. I reported about this to [Google Issue Tracker](https://issuetracker.google.com/issues/72238499). + ### Help ~~~ $ ggsrun u -h @@ -966,11 +975,15 @@ 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 + --filename value, -f value File Name on local PC. Please input files you want to upload. + --parentfolderid value, -p value Folder ID of parent folder on Google Drive + --parentid value, --pid value File ID of Google Docs (Spreadsheet, Document, Slide, Form) for creating container bound-script. + --timezone value, --tz value Time zone of project. Please use this together with creating new project. When new project is created by API, time zone doesn't become the local time zone. (This might be a bug.) So please input this. + --projectname value, --pn value Upload several GAS scripts as a project. + --googledocname value, --gn value Filename of Google Docs which is created. + --projecttype value, --pt value You can select where it creates a new project. Please input 'spreadsheet', 'document', 'slide' and 'form'. When you select one of them, new project is created as a bound script. If this option is not used, new project is created as a standalone script. This is a default. (default: "standalone") + --noconvert, --nc If you don't want to convert file to Google Apps format. + --jsonparser, -j Display results by JSON parser ~~~ @@ -1044,9 +1057,11 @@ $ ggsrun ls -si [file id] -j Result includes file name, file id, modified time and URL. +> **Search of scripts :** You can search standalone scripts using filename and fileId. But the container-bound scripts cannot be searched by filename, while it can be searched by fileId. Because parentId cannot be retrieved using Apps Script API yet. About this, I reported about this to [Google Issue Tracker](https://issuetracker.google.com/issues/71941200). + ## 10. Update Project -It updates existing project on Google Drive. +It updates existing project on Google Drive. You can update standalone script and container-bound script. **Run :** @@ -1056,11 +1071,28 @@ $ ggsrun ud -p [Project ID on Google Drive] -f [script .gs, .gas, .htm, .html] If it is not used ``-p``, the project ID is used the script ID in "ggsrun.cfg". When a script for updating is the same to a script name in the project, it is overwritten. Other scripts in the project is not changed. So this can be also used for updating a script in the project. +**Delete files in project** + +You can also delete files in a project. + +~~~bash +$ ggsrun ud -f [filename in project] -p [Project ID on Google Drive] --deletefiles +~~~ + +> **IMPORTANT :** If you use this option, at first, please test using a sample project. By this, please use this after you understand the work of this. + +### Demo for creating bound script in Google Docs Updated! (v1.4.0) + +![](images/demo_update.gif) + +In this demonstration, create new Spreadsheet and upload 5 files as new project of bound script. And then, rearrange files in the new project. + + -## 11. Retrieve Revision Files -It retrieves revisions for files on Google Drive. +## 11. Retrieve Revision Files and Versions of Projects +It retrieves revisions for files on Google Drive and retrieves versions for projects. -**Display revision ID list for file ID :** +**Display revision and version ID list for file ID :** ~~~bash $ ggsrun r -i [File ID] @@ -1075,7 +1107,7 @@ When above command is run, you can see the revision list and extensions which ca ] ~~~ -**Download revision file using revision ID :** +**Download revision and version file using revision ID :** ~~~bash $ ggsrun r -i [File ID] -d [Revision ID] -e [Extension] @@ -1099,15 +1131,24 @@ You can see the detail information about revision files at following gists. In this demonstration, 2 revision files of spreadsheet are retrieved. +**Create versions of projects** +You can create versions of projects. Created versions are reflected to version list. + +~~~bash +$ ggsrun r -i [File ID] -cv [description of version] +~~~ + + ## 12. Rearrange Script in Project -Have you ever thought about rearranging Google Apps Scripts in a project which can be seen at the script editor? I also have thought about it. Finally, I could find the workaround to do it. From ggsrun with v1.3.2, scripts in a project can be rearranged. +Have you ever thought about rearranging Google Apps Scripts in a project which can be seen at the script editor? I also have thought about it. Finally, I could find the workaround to do it. From ggsrun with v1.3.2, scripts in a project can be rearranged. And also, from v1.4.0, it gets to be able to rearrange both standalone script and container-bound script. If you want to rearrange using a GUI application, please check [RearrangeScripts](https://github.com/tanaikech/RearrangeScripts). #### IMPORTANT! > 1. For rearranging scripts, there is one important point. **When scripts in a project is rearranged, version history of scripts is reset once. So if you don't want to reset the version history, before rearranging, please copy the project.** By copying project, the project before rearranging is saved. -> 2. The rearrangement of scripts can be done for only standalone scripts. Because although the bound scripts can retrieve scripts, it cannot be updated. +> 2. From v1.4.0, it got to be able to rearrange both standalone script and container-bound script. +> 3. The file of ``appsscript`` for Manifests is displayed to the top of files on the script editor, while the array of files can be changed. I think that this is the specification. ### Interactively rearrange scrips on own terminal @@ -1526,7 +1567,7 @@ The GAS script can be modified by the received values from Google. I think that # Appendix ## A1. Scopes -As the default, 6 scopes are set as follows. +As the default, 7 scopes are set in ggsrun as follows. - https://www.googleapis.com/auth/drive - https://www.googleapis.com/auth/drive.file @@ -1534,6 +1575,7 @@ As the default, 6 scopes are set as follows. - https://www.googleapis.com/auth/script.external_request - https://www.googleapis.com/auth/script.scriptapp - https://www.googleapis.com/auth/spreadsheets +- https://www.googleapis.com/auth/script.projects If you want to change the scopes, @@ -1547,6 +1589,8 @@ If the SCOPEs have changed, modify them in 'ggsrun.cfg' and delete a line of 're Please confirm Scopes, which is used at your script, at the Script Editor on Google. +> ``https://www.googleapis.com/auth/script.projects`` was added from v.1.4.0. + ## 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 @@ -1695,6 +1739,11 @@ In such case, please confirm whether ``;`` with end of each line in the script i #### 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. + +#### 7. Order of directories for searching +- About the order of directories for searching ``client_secret.json`` and ``ggsrun.cfg``, at first, files are searched in the current working directory, and next, they are searched in the directory declared by the environment variable of ``GGSRUN_CFG_PATH``. +- About uploading and downloded files, the current working directory is used as the default. + # Update History You can see the Update History at **[here](UpdateHistory.md)**. diff --git a/help/UpdateHistory.md b/help/UpdateHistory.md index bc9b7cb..8366356 100644 --- a/help/UpdateHistory.md +++ b/help/UpdateHistory.md @@ -73,6 +73,33 @@ ggsrun 1. Removed a bug. - When a project is downloaded, script ID in the project is added to the top of each downloaded script as a comment. There was a problem at the character using for the comment out. This was modified. + +* v1.4.0 (January 25, 2018) + + [Google Apps Script API](https://developers.google.com/apps-script/api/reference/rest/) was finally released. From this version, ggsrun uses this API. So ggsrun got to be able to use not only projects of standalone script type, but also projects of container-bound script type. I hope this updated ggsrun will be useful for you. + + 1. **[To users which are using ggsrun with v1.3.4 and/or less](https://github.com/tanaikech/ggsrun/blob/master/README.md#from134to140).** + 1. For retrieving, downloading, creating and updating projects, [Apps Script API](https://developers.google.com/apps-script/api/reference/rest/) is used. + - About retrieving information of projects, the information from Drive API is more than that from Apps Script API. So I used Drive API in this situation. + - **[Please read how to enable APIs.](https://github.com/tanaikech/ggsrun/blob/master/README.md#BasicSettingFlow)** + 1. ggsrun got to be able to use both standalone scripts and container-bound scripts by Apps Script API. + - [Create projects](README.md#UploadFiles) + - [Update projects](README.md#Update_Project) + - There are some issues for creating projects. + 1. After Manifests was added to GAS, the time zone can be set by it. But when a new project is created by API, I noticed that the time zone is different from own local time zone. When a new project is manually created by browser, the time zone is the same to own local time zone. I think that this may be a bug. So I added an option for setting time zone when a new project is created. And also I reported about this to [Google Issue Tracker](https://issuetracker.google.com/issues/72019223). + 1. If you want to create a bound script in Slide, an error occurs. When a bound script can be created to Spreadsheet, Document and Form using Apps Script API. Furthermore, when the bound script in Slide is updated, it works fine. So I think that this may be also a bug. I reported about this to [Google Issue Tracker](https://issuetracker.google.com/issues/72238499). + - About this, when you create a bound script in Slides, if ggsrun returns no errors, it means that this issue was solved. + 1. [Both standalone scripts and container-bound scripts can be rearranged.](README.md#rearrangescripts) + - The file of ``appsscript`` for Manifests is always displayed to the top of files on the script editor, while the array of files can be changed. I think that this is the specification. + 1. For the option ``exe1`` for executing GAS, it can use for both standalone scripts and container-bound scripts. + 1. [Delete files using file ID on Google Drive.](README.md#DownloadFiles) + 1. [Delete files in the project.](README.md#Update_Project) + 1. [ggsrun can create new container-bound script in the new Google Docs.](README.md#UploadFiles) + - For example, ggsrun creates a new Spreadsheet and uploads the script files to the Spreadsheet as a container-bound script. + 1. [Retrieve and create versions of projects.](README.md#RevisionFile) + 1. [Unified the order of directories for searching ``client_secret.json`` and ``ggsrun.cfg``.](README.md#QA7) + 1. Some modifications. + **You can read "How to install" at [here](https://github.com/tanaikech/ggsrun/blob/master/README.md#How_to_Install).** ## Server diff --git a/help/images/demo_update.gif b/help/images/demo_update.gif new file mode 100644 index 0000000..b320c5f Binary files /dev/null and b/help/images/demo_update.gif differ diff --git a/init.go b/init.go index 96223f1..65c251b 100644 --- a/init.go +++ b/init.go @@ -12,18 +12,18 @@ import ( "github.com/urfave/cli" ) -// GgsrunIni : +// GgsrunIni : Initialize ggsrun func (a *AuthContainer) ggsrunIni(c *cli.Context) *AuthContainer { - if cfgdata, err := ioutil.ReadFile(filepath.Join(a.InitVal.cfgdir, cfgFile)); err == nil { + if cfgdata, err := a.chkInitFile(cfgFile); err == nil { err = json.Unmarshal(cfgdata, &a.GgsrunCfg) if err != nil { - fmt.Fprintf(os.Stderr, "Error: Format error of '%s'. ", cfgFile) + fmt.Fprintf(os.Stderr, "Error: Format error of '%s'.\n", 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]'. ") + fmt.Fprintf(os.Stderr, "Error: No script id. Please use option '-i [Script ID]'.\n") os.Exit(1) } if len(c.String("scriptid")) > 0 { @@ -40,19 +40,38 @@ func (a *AuthContainer) ggsrunIni(c *cli.Context) *AuthContainer { return a } +// readClientSecret : Read client secret file func (a *AuthContainer) readClientSecret() *AuthContainer { - if csecret, err := ioutil.ReadFile(filepath.Join(a.InitVal.workdir, clientsecretFile)); err == nil { + if csecret, err := a.chkInitFile(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) + fmt.Fprintf(os.Stderr, "Error: Please confirm '%s'.\nError is %s.\n", 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) + fmt.Fprintf(os.Stderr, "Error: No materials for retrieving accesstoken. Please download '%s'.\n", clientsecretFile) os.Exit(1) } return a } + +// chkInitFile : Check initial files. +// By this method, at first, files are searched in working directory, and next, they are searched in the directory declared by the environment variable. +func (a *AuthContainer) chkInitFile(file string) ([]byte, error) { + var err error + var body []byte + if body, err = ioutil.ReadFile(filepath.Join(a.InitVal.workdir, file)); err == nil { + a.InitVal.usedDir = "work" + return body, err + } + if a.InitVal.workdir != a.InitVal.cfgdir { + if body, err = ioutil.ReadFile(filepath.Join(a.InitVal.cfgdir, file)); err == nil { + a.InitVal.usedDir = "env" + return body, err + } + } + return nil, fmt.Errorf("Error: %s was not found.\n", file) +} diff --git a/materials.go b/materials.go index 324b76b..c02a568 100644 --- a/materials.go +++ b/materials.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "regexp" + "strings" "time" "github.com/tanaikech/ggsrun/utl" @@ -32,12 +33,13 @@ const ( 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/" + 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/" + appsscriptapi = "https://script.googleapis.com/v1/projects" ) // InitVal : Initial values @@ -45,6 +47,7 @@ type InitVal struct { pstart time.Time workdir string cfgdir string + usedDir string // "work" for working directory or "env" for directory declared by the environment variable. update bool log bool Port int @@ -124,15 +127,32 @@ type Com struct { // Project : Project for uploading using Drive API type Project struct { - Files []File `json:"files"` + ScriptId string `json:"scriptId,omitempty"` + 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"` + ID string `json:"id,omitempty"` + Name string `json:"name"` + Type string `json:"type"` + Source string `json:"source"` + CreateTime string `json:"createTime,omitempty"` + UpdateTime string `json:"updateTime,omitempty"` + Creator *creator `json:"creator,omitempty"` + LastModifyUser *lastmodifyuser `json:"lastModifyUser,omitempty"` +} + +// creator : Creator +type creator struct { + Email string `json:"email,omitempty"` + Name string `json:"name,omitempty"` +} + +// lastmodifyuser : lastModifyUser +type lastmodifyuser struct { + Email string `json:"email,omitempty"` + Name string `json:"name,omitempty"` } // FeedBackData : Feedbacked data from function using Execution API (modified) @@ -266,9 +286,12 @@ func defAuthContainer(c *cli.Context) *AuthContainer { 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. + // + // From v1.4.0, https://www.googleapis.com/auth/script.projects was added to scope. a.GgsrunCfg.Scopes = []string{ "https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/drive.file", @@ -276,6 +299,7 @@ func defAuthContainer(c *cli.Context) *AuthContainer { "https://www.googleapis.com/auth/script.external_request", "https://www.googleapis.com/auth/script.scriptapp", "https://www.googleapis.com/auth/spreadsheets", + "https://www.googleapis.com/auth/script.projects", } return a } @@ -353,7 +377,40 @@ func (a *AuthContainer) defUploadContainer(c *cli.Context) *utl.FileInf { Accesstoken: a.GgsrunCfg.Accesstoken, Workdir: a.InitVal.workdir, PstartTime: a.InitVal.pstart, - UpFilename: regexp.MustCompile(`\s*,\s*`).Split(c.String("filename"), -1), + UpFilename: func(filenames string) []string { + if filenames != "" { + return regexp.MustCompile(`\s*,\s*`).Split(filenames, -1) + } else { + return nil + } + }(c.String("filename")), + ParentID: c.String("parentid"), + ProjectType: func(ptype string) string { + var ret string + switch strings.ToLower(ptype) { + case "spreadsheet", "spreadsheets", "sheet", "sheets": + ret = "spreadsheet" + case "document", "documents", "doc": + ret = "document" + case "slide", "slides": + ret = "slide" + case "form": + ret = "form" + default: + ret = ptype + } + return ret + }(c.String("projecttype")), + GoogleDocName: c.String("googledocname"), + } + return p +} + +// dispUpdateProjectContainer : Struct container for downloading files by GAS +func (e *ExecutionContainer) dispUpdateProjectContainer() *utl.FileInf { + p := &utl.FileInf{ + Msgar: e.Msg, + TotalEt: math.Trunc(time.Now().Sub(e.InitVal.pstart).Seconds()*1000) / 1000, } return p } @@ -377,11 +434,23 @@ func (e *ExecutionContainer) defUpdateProjectContainer(c *cli.Context) *Executio return e } -// dispUpdateProjectContainer : Struct container for downloading files by GAS -func (e *ExecutionContainer) dispUpdateProjectContainer() *utl.FileInf { +// convExecutionContainerToFileInf : Convert ExecutionContainer to FileInf +func (e *ExecutionContainer) convExecutionContainerToFileInf() *utl.FileInf { p := &utl.FileInf{ - Msgar: e.Msg, - TotalEt: math.Trunc(time.Now().Sub(e.InitVal.pstart).Seconds()*1000) / 1000, + Accesstoken: e.Accesstoken, } return p } + +// adaptProjectForAppsScriptApi : Adapt project for Apps Script Api +func (e *ExecutionContainer) adaptProjectForAppsScriptApi() *ExecutionContainer { + // e.Project.ScriptId = "" + for i, f := range e.Project.Files { + e.Project.Files[i].Type = strings.ToLower(f.Type) + e.Project.Files[i].CreateTime = "" + e.Project.Files[i].UpdateTime = "" + e.Project.Files[i].Creator = nil + e.Project.Files[i].LastModifyUser = nil + } + return e +} diff --git a/oauth.go b/oauth.go index 2bf5f4e..a161cd8 100644 --- a/oauth.go +++ b/oauth.go @@ -48,7 +48,16 @@ func (a *AuthContainer) reAuth() { // makecfgfile : func (a *AuthContainer) makecfgfile() { btok, _ := json.MarshalIndent(a.GgsrunCfg, "", "\t") - ioutil.WriteFile(filepath.Join(a.InitVal.cfgdir, cfgFile), btok, 0777) + var path string + if a.InitVal.usedDir == "work" { + path = a.InitVal.workdir + } else if a.InitVal.usedDir == "env" { + path = a.InitVal.cfgdir + } else { + fmt.Fprintf(os.Stderr, "Error: directory. '%s'\n", a.InitVal.usedDir) + os.Exit(1) + } + ioutil.WriteFile(filepath.Join(path, cfgFile), btok, 0777) } // getAtoken : Retrieves accesstoken from refreshtoken. diff --git a/projectupdater.go b/projectupdater.go index c20732e..673c133 100644 --- a/projectupdater.go +++ b/projectupdater.go @@ -3,7 +3,7 @@ package main import ( - "bufio" + "encoding/json" "fmt" "os" "path/filepath" @@ -18,11 +18,19 @@ func (e *ExecutionContainer) projectUpdateControl(c *cli.Context) *utl.FileInf { if len(c.String("projectid")) > 0 { e.GgsrunCfg.Scriptid = c.String("projectid") if len(c.String("filename")) > 0 { - return e.defUpdateProjectContainer(c). - projectBackup(c). - ProjectMaker(). - projectUpdate(). - dispUpdateProjectContainer() + if !c.Bool("deletefiles") { + return e.defUpdateProjectContainer(c). + projectBackup(c). + ProjectMaker(). + projectUpdate2(). + dispUpdateProjectContainer() + } else { + return e.defUpdateProjectContainer(c). + projectBackup(c). + filesInProjectRemover(). + projectUpdate2(). + dispUpdateProjectContainer() + } } if c.Bool("rearrange") { e.defUpdateProjectContainer(c). @@ -30,59 +38,41 @@ func (e *ExecutionContainer) projectUpdateControl(c *cli.Context) *utl.FileInf { rearrangeByTerminal() } if len(c.String("rearrangewithfile")) > 0 { - var data []string - f, err := os.Open(c.String("rearrangewithfile")) - if err != nil { - fmt.Fprintf(os.Stderr, "Error: Script '%s' is not found.\n", c.String("rearrangewithfile")) - os.Exit(1) - } - defer f.Close() - scanner := bufio.NewScanner(f) - for scanner.Scan() { - if scanner.Text() == "end" { - break - } - if scanner.Text() != "" { - data = append(data, scanner.Text()) - } - } - if scanner.Err() != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", scanner.Err()) - os.Exit(1) - } + data := getRearrangeTemplate(c.String("rearrangewithfile")) e.defUpdateProjectContainer(c). projectBackup(c). rearrangeByFile(data) } } else { - e.Msg = append(e.Msg, "Error: No options. Please check HELP using 'ggsrun ud -help'.") + e.Msg = append(e.Msg, "Error: No options. Please check HELP using 'ggsrun ud --help'.") } return e.dispUpdateProjectContainer() } +// projectUpdateForBoundScript : Update bound-script project +func (e *ExecutionContainer) projectUpdateForBoundScript() *ExecutionContainer { + p := e.convExecutionContainerToFileInf() + var pr *utl.ProjectForAppsScriptApi + var pp *utl.FilesForAppsScriptApi + pr.ScriptId = e.Project.ScriptId + for _, f := range e.Project.Files { + pp.Name = f.Name + pp.Type = f.Type + pp.Source = f.Source + pr.Files = append(pr.Files, *pp) + } + _ = p.ProjectUpdateByAppsScriptApi(pr) + e.Msg = append(e.Msg, "Project was updated.") + return e +} + // ProjectMaker : Recreates the project using uploaded scripts. func (e *ExecutionContainer) ProjectMaker() *ExecutionContainer { for _, elm := range e.UpFiles { - if filepath.Ext(elm) == ".gs" || - filepath.Ext(elm) == ".gas" || - filepath.Ext(elm) == ".js" || - filepath.Ext(elm) == ".htm" || - filepath.Ext(elm) == ".html" || - filepath.Ext(elm) == ".json" { + if utl.ChkExtention(filepath.Ext(elm)) { filedata := &File{ - 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" - case ".json": - scripttype = "json" - } - return scripttype - }(filepath.Ext(elm)), + Name: strings.Replace(filepath.Base(elm), filepath.Ext(elm), "", -1), + Type: utl.ExtToType(filepath.Ext(elm), false), Source: utl.ConvGasToUpload(elm), } var overwrite bool @@ -96,8 +86,68 @@ func (e *ExecutionContainer) ProjectMaker() *ExecutionContainer { if !overwrite { e.Project.Files = append(e.Project.Files, *filedata) } + } else { + e.Msg = append(e.Msg, fmt.Sprintf("File of '%s' cannot be used for updating project.", elm)) } } - e.Msg = append(e.Msg, fmt.Sprintf("Project ID '%s' was uploaded.", e.Scriptid)) + p := e.convExecutionContainerToFileInf() + body, err, _ := p.ChkBoundOrStandalone(e.GgsrunCfg.Scriptid) + if err == nil { + json.Unmarshal(body, &p) + e.Msg = append(e.Msg, fmt.Sprintf("Filename is '%s'.", p.FileName)) + } + e.Msg = append(e.Msg, fmt.Sprintf("Project ID is '%s'.", e.Scriptid)) return e } + +// filesInProjectRemover : Remove files in project. +func (e *ExecutionContainer) filesInProjectRemover() *ExecutionContainer { + temp := &Project{} + temp = e.Project + var outr []string + for _, elm := range e.UpFiles { + res, removed := removeEle(temp, elm) + if removed { + outr = append(outr, elm) + } + temp = res + } + if len(temp.Files) == 1 { + fmt.Fprintf(os.Stderr, "Error: You cannot remove all files except for 'appsscript.json' in the project.\n") + os.Exit(1) + } + e.Project = temp + p := e.convExecutionContainerToFileInf() + body, err, _ := p.ChkBoundOrStandalone(e.GgsrunCfg.Scriptid) + if err == nil { + json.Unmarshal(body, &p) + e.Msg = append(e.Msg, fmt.Sprintf("Filename is '%s'.", p.FileName)) + } + e.Msg = append(e.Msg, fmt.Sprintf("Project ID is '%s'.", e.Scriptid)) + if len(outr) == 0 { + fmt.Fprintf(os.Stderr, "[ %s ] were not found in the project. No files were removed from the project.\n", strings.Join(e.UpFiles, ", ")) + os.Exit(1) + } else { + e.Msg = append(e.Msg, fmt.Sprintf("Files of [ %s ] were removed from the project.", strings.Join(outr, ", "))) + } + return e +} + +// removeEle : Remove an element from an array. +func removeEle(project *Project, elm string) (*Project, bool) { + temp := &Project{} + ff := strings.Replace(filepath.Base(elm), filepath.Ext(elm), "", -1) + if ff != "appsscript" { + for _, v := range project.Files { + if v.Name != ff { + temp.Files = append(temp.Files, v) + } + } + } else { + return project, false + } + if len(project.Files) != len(temp.Files) { + return temp, true + } + return temp, false +} diff --git a/scriptrearrange.go b/scriptrearrange.go index 613833e..eab3171 100644 --- a/scriptrearrange.go +++ b/scriptrearrange.go @@ -3,6 +3,7 @@ package main import ( + "bufio" "fmt" "os" "strconv" @@ -14,7 +15,7 @@ import ( ) // rearrangeByTerminal : Rearranging scripts in a project using go-rearrange. -func (e *ExecutionContainer) rearrangeByTerminal() { +func (e *ExecutionContainer) rearrangeByTerminal() *ExecutionContainer { var baseProject Project baseProject = *e.Project var scripts []string @@ -42,15 +43,15 @@ func (e *ExecutionContainer) rearrangeByTerminal() { e.rearrange(baseProject, changedIndx) s.Stop() fmt.Printf("\n") - return + return e } else { e.Msg = append(e.Msg, "Scripts of project were NOT rearranged.") - return + return e } } // rearrange : Rearranging scripts in a project using a configuration file. -func (e *ExecutionContainer) rearrangeByFile(data []string) { +func (e *ExecutionContainer) rearrangeByFile(data []string) *ExecutionContainer { var baseProject Project baseProject = *e.Project var temp []string @@ -82,22 +83,22 @@ func (e *ExecutionContainer) rearrangeByFile(data []string) { } if cn == len(e.Project.Files) { e.rearrange(baseProject, changedIndx) - return + return e } else { e.Msg = append(e.Msg, "Error: Script names of inputted file are different for script names in project.") - return + return e } } else { e.Msg = append(e.Msg, "Error: Order of inputted file are the same to the order in project.") - return + return e } } else { e.Msg = append(e.Msg, "Error: Number of script names of inputted file are different for number of scripts in project.") - return + return e } } else { e.Msg = append(e.Msg, "Error: There are duplicated names in script names of inputted file.") - return + return e } } @@ -106,22 +107,25 @@ func (e *ExecutionContainer) rearrange(baseProject Project, changedIndx []string var temp1 Project const layout = "20060102_150405_" t := time.Now() - dummyScript := &File{ + temp1.Files = append(temp1.Files, File{ Name: "Dummy_" + t.Format(layout) + t.AddDate(0, 0, 2).Weekday().String(), Source: "// This is a dummy.", - Type: "server_js", - } - temp1.Files = append(temp1.Files, *dummyScript) + Type: "SERVER_JS", + }) + temp1.Files = append(temp1.Files, File{ + Name: "appsscript", + Source: "{}", + Type: "JSON", + }) e.Project = &temp1 - e.projectUpdate() + e.projectUpdate2() var temp2 Project - for i, e := range changedIndx { + for _, e := range changedIndx { idx, _ := strconv.Atoi(e) temp2.Files = append(temp2.Files, baseProject.Files[idx]) - temp2.Files[i].ID = "" } e.Project = &temp2 - e.projectUpdate() + e.projectUpdate2() var from, to []string for i, f := range e.Project.Files { from = append(from, baseProject.Files[i].Name) @@ -130,3 +134,28 @@ func (e *ExecutionContainer) rearrange(baseProject Project, changedIndx []string msg := fmt.Sprintf("Scripts in project were rearranged from [%s] to [%s].", strings.Join(from, ", "), strings.Join(to, ", ")) e.Msg = []string{msg} } + +// getRearrangeTemplate : Retrieve data from template file for rearranging. +func getRearrangeTemplate(templateFile string) []string { + var data []string + f, err := os.Open(templateFile) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: Script '%s' is not found.\n", templateFile) + os.Exit(1) + } + defer f.Close() + scanner := bufio.NewScanner(f) + for scanner.Scan() { + if scanner.Text() == "end" { + break + } + if scanner.Text() != "" { + data = append(data, scanner.Text()) + } + } + if scanner.Err() != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", scanner.Err()) + os.Exit(1) + } + return data +} diff --git a/sender.go b/sender.go index 3f58428..5703f61 100644 --- a/sender.go +++ b/sender.go @@ -13,6 +13,7 @@ import ( "net/textproto" "net/url" "os" + "path" "path/filepath" "regexp" "strconv" @@ -26,7 +27,7 @@ import ( // Exe1Function : func (e *ExecutionContainer) exe1Function(c *cli.Context) *ExecutionContainer { if len(c.String("scriptfile")) > 0 || c.Bool("backup") { - return e.projectBackup(c).projectUpdateIni(utl.ConvGasToPut(c)).projectUpdate() + return e.projectBackup(c).projectUpdateIni(utl.ConvGasToPut(c)).projectUpdate2() } return e } @@ -234,6 +235,7 @@ func (e *ExecutionContainer) esenderForExe2(c *cli.Context) *ExecutionContainer return e } +// projectUpdateIni : Initialize for updating project func (e *ExecutionContainer) projectUpdateIni(sendscript string) *ExecutionContainer { var overwrite bool for i := range e.Project.Files { @@ -245,7 +247,7 @@ func (e *ExecutionContainer) projectUpdateIni(sendscript string) *ExecutionConta if !overwrite { filedata := &File{ Name: defprojectname, - Type: "server_js", + Type: "SERVER_JS", Source: sendscript, } e.Project.Files = append(e.Project.Files, *filedata) @@ -253,7 +255,7 @@ func (e *ExecutionContainer) projectUpdateIni(sendscript string) *ExecutionConta return e } -// ProjectUpdate : +// ProjectUpdate : In this method, the project is updated using Drive API. func (e *ExecutionContainer) projectUpdate() *ExecutionContainer { script, _ := json.Marshal(e.Project) metadata, _ := json.Marshal(&ProjectUpdaterMeta{MimeType: "application/vnd.google-apps.script"}) @@ -300,19 +302,50 @@ func (e *ExecutionContainer) projectUpdate() *ExecutionContainer { return e } -// ProjectBackup : +// projectUpdate2 : In this method, the project is updated using Apps Script API. +func (e *ExecutionContainer) projectUpdate2() *ExecutionContainer { + script, _ := json.Marshal(e.Project) + tokenparams := url.Values{} + tokenparams.Set("fields", "files,scriptId") + u, _ := url.Parse(appsscriptapi) + u.Path = path.Join(u.Path, e.GgsrunCfg.Scriptid+"/content") + r := &utl.RequestParams{ + Method: "PUT", + APIURL: u.String() + "?" + tokenparams.Encode(), + Data: bytes.NewBuffer(script), + Accesstoken: e.GgsrunCfg.Accesstoken, + Dtime: 30, + } + res, err := r.FetchAPI() + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v. ", err) + utl.DispScopeError2(res) + os.Exit(1) + } + e.Msg = append(e.Msg, "Project was updated.") + _ = res // Now, no results are returned. + return e +} + +// ProjectBackup : Download and backup project (Apps Script API v1) func (e *ExecutionContainer) projectBackup(c *cli.Context) *ExecutionContainer { + tokenparams := url.Values{} + tokenparams.Set("fields", "files,scriptId") + u, _ := url.Parse(appsscriptapi) + u.Path = path.Join(u.Path, e.GgsrunCfg.Scriptid+"/content") r := &utl.RequestParams{ Method: "GET", - APIURL: sdownloadurl + e.GgsrunCfg.Scriptid + "&format=json", + APIURL: u.String() + "?" + tokenparams.Encode(), Data: nil, - Contenttype: "", + Contenttype: "application/x-www-form-urlencoded", Accesstoken: e.GgsrunCfg.Accesstoken, - Dtime: 10, + Dtime: 30, } res, err := r.FetchAPI() if err != nil { - fmt.Fprintf(os.Stderr, "Error: %v. Please check project ID. Inputted project ID is '%s'.\n", err, e.Scriptid) + fmt.Fprintf(os.Stderr, "Error: %v.\n%v\n\n", err, string(res)) + fmt.Fprintf(os.Stderr, "One of reasons of error :\n Was the inputted project ID correct?.\n") + utl.DispScopeError2(res) os.Exit(1) } json.Unmarshal(res, &e.Project) diff --git a/utl/appsscriptapi.go b/utl/appsscriptapi.go new file mode 100644 index 0000000..d2f8142 --- /dev/null +++ b/utl/appsscriptapi.go @@ -0,0 +1,379 @@ +// Package utl (appsscriptapi.go) : +// These methods are for using Apps Script API. +package utl + +import ( + "bytes" + "encoding/json" + "fmt" + "net/url" + "os" + "path" + "path/filepath" + "strconv" + "strings" + "time" +) + +const ( + appsscriptapi = "https://script.googleapis.com/v1/projects" +) + +// AppsScriptApiInf : Information retrieved by Apps Script API +type AppsScriptApiInf struct { + ScriptId string `json:"scriptId"` + ParentId string `json:"parentId"` + Title string `json:"title"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` + Creator creator `json:"creator"` + LastModifyUser lastmodifyuser `json:"lastModifyUser"` +} + +// creator : Creator +type creator struct { + Email string `json:"email"` + Name string `json:"name"` +} + +// lastmodifyuser : lastModifyUser +type lastmodifyuser struct { + Email string `json:"email"` + Name string `json:"name"` +} + +// ProjectForAppsScriptApi : Project structure for Apps Script API +type ProjectForAppsScriptApi struct { + ScriptId string `json:"scriptId,omitempty"` + Files []FilesForAppsScriptApi `json:"files"` +} + +// FilesForAppsScriptApi : A file structure for Apps Script API +type FilesForAppsScriptApi struct { + Name string `json:"name"` + Type string `json:"type"` + Source string `json:"source"` + CreateTime time.Time `json:"createTime,omitempty"` + UpdateTime time.Time `json:"updateTime,omitempty"` + Creator *creator `json:"creator,omitempty"` + LastModifyUser *lastmodifyuser `json:"lastModifyUser,omitempty"` +} + +// manifestsStruct : Struct of Manifests +type manifestsStruct struct { + TimeZone string `json:"timeZone,omitempty"` + OauthScopes []interface{} `json:"oauthScopes,omitempty"` + Dependencies interface{} `json:"dependencies,omitempty"` + ExceptionLogging interface{} `json:"exceptionLogging,omitempty"` + Webapp interface{} `json:"webapp,omitempty"` + ExecutionApi interface{} `json:"executionApi,omitempty"` + UrlFetchWhitelist []interface{} `json:"urlFetchWhitelist,omitempty"` + Gmail interface{} `json:"gmail,omitempty"` +} + +// projectVersionList : Struct for version list of project +type projectVersionList struct { + Versions []struct { + ScriptId string `json:"scriptId"` + VersionNumber int `json:"versionNumber"` + Description string `json:"description"` + CreateTime time.Time `json:"createTime"` + } `json:"versions"` + NextPageToken string `json:"nextPageToken,omitempty"` +} + +// createVersionSt : Struct for creating version +type createVersionSt struct { + VersionNumber string `json:"versionNumber,omitempty"` + Description string `json:"description"` + CreateTime string `json:"createTime,omitempty"` +} + +// getBoundScriptInf : Retrieve information of boundscript. +func (p *FileInf) getBoundScriptInf(id string) { + tokenparams := url.Values{} + tokenparams.Set("fields", "createTime,creator,lastModifyUser,parentId,scriptId,title,updateTime") + r := &RequestParams{ + Method: "GET", + APIURL: appsscriptapi + "/" + id + "?" + tokenparams.Encode(), + Data: nil, + Contenttype: "application/x-www-form-urlencoded", + Accesstoken: p.Accesstoken, + Dtime: 30, + } + body, err := r.FetchAPI() + if err != nil { + fmt.Fprintf(os.Stderr, "Error: File ID '%s' is not found. ", id) + DispScopeError2(body) + os.Exit(1) + } + var i *AppsScriptApiInf + json.Unmarshal(body, &i) + p.FileName = i.Title + p.MimeType = "application/vnd.google-apps.script" + p.ParentID = i.ParentId + p.FileID = i.ScriptId + o := &owners{} + o.Email = i.Creator.Email + o.Name = i.Creator.Name + p.Owners = append(p.Owners, *o) + if i.LastModifyUser.Email != "" || i.LastModifyUser.Name != "" { + lmu := &lastmodifieduser{ + i.LastModifyUser.Email, + i.LastModifyUser.Name, + } + p.LastModifyingUser = lmu + } +} + +// getBoundScript : Retrieve boundscript. +func (p *FileInf) getBoundScript(id string) *ProjectForAppsScriptApi { + tokenparams := url.Values{} + tokenparams.Set("fields", "files,scriptId") + u, _ := url.Parse(appsscriptapi) + u.Path = path.Join(u.Path, id+"/content") + r := &RequestParams{ + Method: "GET", + APIURL: u.String() + "?" + tokenparams.Encode(), + Data: nil, + Contenttype: "application/x-www-form-urlencoded", + Accesstoken: p.Accesstoken, + Dtime: 30, + } + body, err := r.FetchAPI() + if err != nil { + fmt.Fprintf(os.Stderr, "Error: File ID '%s' is not found. ", p.SearchByID) + DispScopeError2(body) + os.Exit(1) + } + pf := &ProjectForAppsScriptApi{} + json.Unmarshal(body, &pf) + return pf +} + +// boundScriptCreator : Create container bound-scripts in Google Docs. +func (p *FileInf) boundScriptCreator(metadata []byte) *AppsScriptApiInf { + tokenparams := url.Values{} + tokenparams.Set("fields", "createTime,creator,lastModifyUser,parentId,scriptId,title,updateTime") + r := &RequestParams{ + Method: "POST", + APIURL: appsscriptapi + "?" + tokenparams.Encode(), + Data: bytes.NewBuffer(metadata), + Accesstoken: p.Accesstoken, + Dtime: 30, + } + body, err := r.FetchAPI() + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v.\n%v\n\n", err, string(body)) + fmt.Fprintf(os.Stderr, "One of reasons of error :\n Was the inputted parent ID correct?.\n") + var u map[string]interface{} + json.Unmarshal(body, &u) + em := u["error"].(map[string]interface{})["message"] + if em == "Request had insufficient authentication scopes." { + DispScopeError1() + } else if em == "Request contains an invalid argument." { + fmt.Fprintf(os.Stderr, " - If this error occurs when you try to create project in Google Slides, this may be a bug. https://issuetracker.google.com/issues/72238499\n") + } + os.Exit(1) + } + var a *AppsScriptApiInf + json.Unmarshal(body, &a) + var uf uploadedFile + uf.ID = a.ScriptId + uf.Name = a.Title + uf.MimeType = "application/vnd.google-apps.script" + p.UppedFiles = append(p.UppedFiles, uf) + return a +} + +// ProjectUpdateByAppsScriptApi : For uploading project using Apps Script API. +func (p *FileInf) ProjectUpdateByAppsScriptApi(pr *ProjectForAppsScriptApi) *AppsScriptApiInf { + pre, _ := json.Marshal(pr) + tokenparams := url.Values{} + tokenparams.Set("fields", "files,scriptId") + u, _ := url.Parse(appsscriptapi) + u.Path = path.Join(u.Path, pr.ScriptId+"/content") + r := &RequestParams{ + Method: "PUT", + APIURL: u.String() + "?" + tokenparams.Encode(), + Data: bytes.NewBuffer(pre), + Accesstoken: p.Accesstoken, + Dtime: 30, + } + body, err := r.FetchAPI() + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v. ", err) + DispScopeError2(body) + os.Exit(1) + } + var asi *AppsScriptApiInf + json.Unmarshal(body, &asi) + return asi +} + +// createProjectForAppsScriptApi : Create json of project for Apps Script API. +func (p *FileInf) createProjectForAppsScriptApi(scriptId string) *ProjectForAppsScriptApi { + pr := &ProjectForAppsScriptApi{} + pr.ScriptId = scriptId + if len(p.UpFilename) > 0 { + for _, elm := range p.UpFilename { + if ChkExtention(filepath.Ext(elm)) { + filedata := &FilesForAppsScriptApi{ + Name: strings.Replace(filepath.Base(elm), filepath.Ext(elm), "", -1), + Type: ExtToType(filepath.Ext(elm), true), + Source: ConvGasToUpload(elm), + } + pr.Files = append(pr.Files, *filedata) + } + } + if len(pr.Files) == 0 { + fmt.Fprintf(os.Stderr, "Error: Inputted files cannot be used for GAS project.\n") + os.Exit(1) + } + } else { + filedata := &FilesForAppsScriptApi{ + Name: "Code", + Type: "SERVER_JS", + Source: "function myFunction() {\n \n}\n", + } + pr.Files = append(pr.Files, *filedata) + } + return pr +} + +// getManifests : Retrieve Manifests from data +func (pf *ProjectForAppsScriptApi) getManifests(timeZone string) *FilesForAppsScriptApi { + manifests := &FilesForAppsScriptApi{} + for _, e := range pf.Files { + if e.Name == "appsscript" && e.Type == "JSON" { + manifests.Name = e.Name + manifests.Type = e.Type + manifests.Source = e.Source + break + } + } + if timeZone != "" { + var mf manifestsStruct + json.Unmarshal([]byte(manifests.Source), &mf) + mf.TimeZone = timeZone + umf, err := json.MarshalIndent(mf, "", " ") + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %s", err) + os.Exit(1) + } + manifests.Source = string(umf) + } + return manifests +} + +// setManifests : Import Manifests to data +func (pf *ProjectForAppsScriptApi) setManifests(manifests *FilesForAppsScriptApi) *ProjectForAppsScriptApi { + chkManifests := func(files []FilesForAppsScriptApi) bool { + for _, e := range files { + if e.Name == "appsscript" && e.Type == "JSON" { + return true + } + } + return false + }(pf.Files) + if !chkManifests { + filedata := &FilesForAppsScriptApi{ + Name: manifests.Name, + Type: manifests.Type, + Source: manifests.Source, + } + pf.Files = append(pf.Files, *filedata) + } + return pf +} + +// getProjectVersionListInit : Initial method for retrieving version list of project. +func (p *FileInf) getProjectVersionListInit() *projectVersionList { + fm := &projectVersionList{} + var fl projectVersionList + var dmy projectVersionList + fm.NextPageToken = "" + for i := 0; ; { + _ = i + body, err := p.getProjectVersionList(fm.NextPageToken) + json.Unmarshal(body, &fl) + fm.NextPageToken = fl.NextPageToken + fm.Versions = append(fm.Versions, fl.Versions...) + fl.NextPageToken = "" + fl.Versions = dmy.Versions + if len(fm.NextPageToken) == 0 || err != nil { + break + } + } + return fm +} + +// getProjectVersionList : Retrieve version list of project. +func (p *FileInf) getProjectVersionList(ptoken string) ([]byte, error) { + number := 100 + tokenparams := url.Values{} + tokenparams.Set("fields", "nextPageToken,versions") + tokenparams.Set("pageSize", strconv.Itoa(number)) + if len(ptoken) > 0 { + tokenparams.Set("pageToken", ptoken) + } + u, _ := url.Parse(appsscriptapi) + u.Path = path.Join(u.Path, p.FileID+"/versions") + r := &RequestParams{ + Method: "GET", + APIURL: u.String() + "?" + tokenparams.Encode(), + Data: nil, + Accesstoken: p.Accesstoken, + Dtime: 30, + } + body, err := r.FetchAPI() + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n%s\n", err, string(body)) + DispScopeError2(body) + os.Exit(1) + } + return body, err +} + +// createProjectVersion : Create new version of GAS project. +func (p *FileInf) createProjectVersion(description string) { + var payload createVersionSt + payload.Description = description + payl, _ := json.Marshal(payload) + u, _ := url.Parse(appsscriptapi) + u.Path = path.Join(u.Path, p.FileID+"/versions") + r := &RequestParams{ + Method: "POST", + APIURL: u.String(), + Data: bytes.NewBuffer(payl), + Accesstoken: p.Accesstoken, + Dtime: 30, + } + body, err := r.FetchAPI() + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n%s\n", err, string(body)) + DispScopeError2(body) + os.Exit(1) + } + var rs map[string]interface{} + json.Unmarshal(body, &rs) + p.Msgar = append(p.Msgar, fmt.Sprintf("New version was created to '%s' as '%d'.", p.FileName, int(rs["versionNumber"].(float64)))) + p.Msgar = append(p.Msgar, fmt.Sprintf("Description is '%s'.", description)) +} + +// DispScopeError1 : Display about new scope of 'https://www.googleapis.com/auth/script.projects'. +func DispScopeError1() { + fmt.Printf("\n\n##########\n") + fmt.Fprintf(os.Stderr, "One of reasons of error :\n - Did you add new scope of 'https://www.googleapis.com/auth/script.projects' to 'ggsrun.cfg'? If this scope is not added yet, please add it, and run below.\n\n $ ggsrun auth\n\n By this, the scope is reflected.\n - And please enable Google Apps Script API at 'https://console.cloud.google.com/apis/library/script.googleapis.com/?project=### project ID ###'\n You can see '### project ID ###' in 'client_secret.json'.\n") + fmt.Printf("##########\n") +} + +// DispScopeError2 : Display about new scope of 'https://www.googleapis.com/auth/script.projects'. +func DispScopeError2(body []byte) { + var u map[string]interface{} + json.Unmarshal(body, &u) + em := u["error"].(map[string]interface{})["message"] + if em == "Request had insufficient authentication scopes." { + DispScopeError1() + } +} diff --git a/utl/dlrevfile.go b/utl/dlrevfile.go index 0beacf7..fece384 100644 --- a/utl/dlrevfile.go +++ b/utl/dlrevfile.go @@ -9,6 +9,7 @@ import ( "math" "net/url" "os" + "path" "strings" "text/tabwriter" "time" @@ -52,19 +53,65 @@ type revisionListv2 struct { // GetRevisionList : Display revision IDs. func (p *FileInf) GetRevisionList(c *cli.Context) *FileInf { - p.GetFileinf() - if p.MimeType == "application/vnd.google-apps.spreadsheet" || - p.MimeType == "application/vnd.google-apps.document" || - p.MimeType == "application/vnd.google-apps.presentation" || - p.MimeType == "application/vnd.google-apps.drawing" { - p.getRevFromGoogleDocs(c) + if c.String("fileid") == "" && c.String("download") == "" && c.String("createversion") == "" { + p.Msgar = append(p.Msgar, "Error: No options. Please check HELP using 'ggsrun r --help'.") } else { - p.getRevFromExGoogleDocs(c) + p.GetFileinf() + if p.MimeType == "application/vnd.google-apps.spreadsheet" || + p.MimeType == "application/vnd.google-apps.document" || + p.MimeType == "application/vnd.google-apps.presentation" || + p.MimeType == "application/vnd.google-apps.drawing" { + p.getRevFromGoogleDocs(c) + } else if p.MimeType == "application/vnd.google-apps.script" || len(p.FileID) == lengthOfProjectId { + p.versionForProject(c) + } else { + p.getRevFromExGoogleDocs(c) + } } p.TotalEt = math.Trunc(time.Now().Sub(p.PstartTime).Seconds()*1000) / 1000 return p } +// versionForProject : Manage versions for project +func (p *FileInf) versionForProject(c *cli.Context) *FileInf { + if c.String("createversion") == "" { + pvl := p.getProjectVersionListInit() + if c.String("download") == "" { + p.dispProjectVersionList(pvl) + } else { + p.FileID = c.String("fileid") + p.RevisionID = c.String("download") + u, _ := url.Parse(appsscriptapi) + u.Path = path.Join(u.Path, p.FileID+"/content") + params := url.Values{} + params.Set("versionNumber", p.RevisionID) + verUrl := u.String() + "?" + params.Encode() + p, body := p.writeFile(verUrl) + p.saveScript(body, c) + } + } else { + p.createProjectVersion(c.String("createversion")) + } + return p +} + +// getVerFromProject : Retrieve version from project +func (p *FileInf) dispProjectVersionList(pvl *projectVersionList) { + ar := pvl.Versions + if len(ar) > 0 { + buffer := &bytes.Buffer{} + w := new(tabwriter.Writer) + w.Init(buffer, 0, 4, 1, ' ', 0) + fmt.Fprintf(w, "\n%s\t%s\t%s\n", "# versionNumber", "# description", "# createTime") + for _, e := range ar { + fmt.Fprintf(w, "%d\t%s\t%s\n", e.VersionNumber, e.Description, e.CreateTime.In(time.Local).Format("20060102_15:04:05")) + } + w.Flush() + fmt.Printf("%s\n", buffer) + } + p.Msgar = append(p.Msgar, fmt.Sprintf("Version list of '%s' was retrieved.", p.FileName)) +} + // getRevFromGoogleDocs : Display revision IDs from Google Docs. func (p *FileInf) getRevFromGoogleDocs(c *cli.Context) { if len(p.FileID) > 0 { diff --git a/utl/transfer.go b/utl/transfer.go index d8ab884..c76b977 100644 --- a/utl/transfer.go +++ b/utl/transfer.go @@ -13,6 +13,7 @@ import ( "net/textproto" "net/url" "os" + "path" "path/filepath" "strconv" "strings" @@ -23,38 +24,55 @@ import ( ) 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/" - driveapiurlv2 = "https://www.googleapis.com/drive/v2/files/" - uploadurl = "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&" + lurl = "https://www.googleapis.com/drive/v3/files?" + driveapiurl = "https://www.googleapis.com/drive/v3/files/" + driveapiurlv2 = "https://www.googleapis.com/drive/v2/files/" + uploadurl = "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&" + lengthOfProjectId = 57 ) // 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"` - ProjectID string `json:"project_id,omitempty"` - BoundScriptName string `json:"_"` - RevisionID string `json:"revisionid,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"` + 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"` + ProjectID string `json:"project_id,omitempty"` + ProjectType string `json:"-"` + ParentID string `json:"parentId,omitempty"` + BoundScriptName string `json:"-"` + GoogleDocName string `json:"-"` + RevisionID string `json:"revisionid,omitempty"` + FileName string `json:"name,omitempty"` + SaveName string `json:"saved_file_name,omitempty"` + LastModifyingUser *lastmodifieduser `json:"lastModifyingUser,omitempty"` + Owners []owners `json:"owners,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"` +} + +// owners : Owners of file +type owners struct { + Name string `json:"displayName,omitempty"` + Email string `json:"emailAddress,omitempty"` +} + +// lastmodifieduser : Last modified user of file +type lastmodifieduser struct { + Name string `json:"displayName,omitempty"` + Email string `json:"emailAddress,omitempty"` } // dlError : Error messages. @@ -83,6 +101,12 @@ type filea struct { Source string `json:"source"` } +// newProject : Create new project +type newProject struct { + ParentId string `json:"parentId,omitempty"` + Title string `json:"title"` +} + // fileListSt : File list. type fileListSt struct { NextPageToken string `json:"nextPageToken,omitempty"` @@ -141,6 +165,7 @@ func (p *FileInf) saveScript(data []byte, c *cli.Context) *FileInf { p.Msgar = append(p.Msgar, fmt.Sprintf("%s has %d scripts.", p.FileName, len(f.Files))) } for _, e := range f.Files { + eType := strings.ToLower(e.Type) saveName := p.FileName + "_" + e.Name + "." + func(ex, ty string) string { var eext string if len(ex) > 0 { @@ -153,20 +178,13 @@ func (p *FileInf) saveScript(data []byte, c *cli.Context) *FileInf { eext = "html" case "json": eext = "json" + default: + eext = "txt" } } return eext - }(p.WantExt, e.Type) - var src string - switch e.Type { - case "server_js": - src = fmt.Sprintf("// Script ID in Project = %s \n%s", e.ID, e.Source) - case "html": - src = fmt.Sprintf("\n%s", e.ID, e.Source) - default: - src = fmt.Sprintf("%s", e.Source) - } - ioutil.WriteFile(filepath.Join(p.Workdir, saveName), []byte(src), 0777) + }(p.WantExt, eType) + ioutil.WriteFile(filepath.Join(p.Workdir, saveName), []byte(e.Source), 0777) p.Msgar = append(p.Msgar, fmt.Sprintf("Script was downloaded as '%s'.", saveName)) } } @@ -181,7 +199,7 @@ func (p *FileInf) Downloader(c *cli.Context) *FileInf { } else { p.DlMime, ext = defFormat(p.MimeType) } - if len(p.FileID) > 0 || len(p.ProjectID) > 0 { + if len(p.FileID) > 0 && c.String("deletefile") == "" { var body []byte var gm map[string]interface{} json.Unmarshal([]byte(googlemimetypes), &gm) @@ -197,29 +215,22 @@ func (p *FileInf) Downloader(c *cli.Context) *FileInf { os.Exit(1) } if p.MimeType == "application/vnd.google-apps.script" { - p, body = p.writeFile(sdownloadurl + p.FileID + "&format=json") + u, _ := url.Parse(appsscriptapi) + u.Path = path.Join(u.Path, p.FileID+"/content") + p, body = p.writeFile(u.String()) p.saveScript(body, c) } else if p.MimeType != "" { p, _ = p.writeFile(driveapiurl + p.FileID + "/export?mimeType=" + p.DlMime) } } else { - if len(p.ProjectID) > 0 && p.MimeType == "" { - if p.BoundScriptName != "" { - p.FileName = p.BoundScriptName - } else { - p.FileName = p.ProjectID - } - p.MimeType = "application/vnd.google-apps.script" - p, body = p.writeFile(sdownloadurl + p.ProjectID + "&format=json") - p.saveScript(body, c) - } else { - p.SaveName = p.FileName - p, _ = p.writeFile(driveapiurl + p.FileID + "?alt=media") - } + p.SaveName = p.FileName + p, _ = p.writeFile(driveapiurl + p.FileID + "?alt=media") } + } else if c.String("deletefile") != "" { + p.deleteFile(c.String("deletefile")) + p.Msgar = append(p.Msgar, fmt.Sprintf("File with fileId '%s' was deleted.", c.String("deletefile"))) } else { - fmt.Fprintf(os.Stderr, "Error: Please input File Name or File ID. ") - os.Exit(1) + p.Msgar = append(p.Msgar, "Error: Please input File Name or File ID. Please check HELP using 'ggsrun d --help'.") } p.TotalEt = math.Trunc(time.Now().Sub(p.PstartTime).Seconds()*1000) / 1000 return p @@ -240,6 +251,9 @@ func (p *FileInf) writeFile(durl string) (*FileInf, []byte) { json.Unmarshal(body, &er) if err != nil || er.Error.Code-300 >= 0 { fmt.Print(fmt.Sprintf("Error: %s. (Status code is %d)\nFileID: %s\n", er.Error.Message, er.Error.Code, p.FileID)) + if er.Error.Message == "Request had insufficient authentication scopes." { + DispScopeError1() + } os.Exit(1) } if p.MimeType != "application/vnd.google-apps.script" { @@ -249,14 +263,32 @@ func (p *FileInf) writeFile(durl string) (*FileInf, []byte) { return p, body } -// nameToID : +// deleteFile : Delete a file using a file ID on own Google Drive. +func (p *FileInf) deleteFile(id string) { + r := &RequestParams{ + Method: "DELETE", + APIURL: driveapiurl + id, + Data: nil, + Contenttype: "application/x-www-form-urlencoded", + Accesstoken: p.Accesstoken, + Dtime: 30, + } + _, err := r.FetchAPI() + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v. ", err) + os.Exit(1) + } + return +} + +// nameToID : Convert filename to file ID 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)") + tokenparams.Set("fields", "files(createdTime,fullFileExtension,id,mimeType,modifiedTime,name,parents,size,webContentLink,webViewLink,lastModifyingUser(displayName,emailAddress),owners(displayName,emailAddress))") r := &RequestParams{ Method: "GET", APIURL: lurl + tokenparams.Encode(), @@ -268,10 +300,10 @@ func (p *FileInf) nameToID(name string) ([]byte, error) { return r.FetchAPI() } -// idToName : Convert file ID to file name. +// idToName : Convert file ID to filename. func (p *FileInf) idToName(id string) ([]byte, error) { tokenparams := url.Values{} - tokenparams.Set("fields", "createdTime,fullFileExtension,id,mimeType,modifiedTime,name,parents,size,webContentLink,webViewLink") + tokenparams.Set("fields", "createdTime,fullFileExtension,id,mimeType,modifiedTime,name,parents,size,webContentLink,webViewLink,lastModifyingUser(displayName,emailAddress),owners(displayName,emailAddress)") r := &RequestParams{ Method: "GET", APIURL: driveapiurl + id + "?" + tokenparams.Encode(), @@ -283,21 +315,32 @@ func (p *FileInf) idToName(id string) ([]byte, error) { return r.FetchAPI() } +// ChkBoundOrStandalone : Check whether the fileId is a bound script or a standalone script. +func (p *FileInf) ChkBoundOrStandalone(fileId string) ([]byte, error, bool) { + body, err := p.idToName(fileId) + if err != nil && len(fileId) == lengthOfProjectId { + return body, err, false + } else if err != nil && len(fileId) < lengthOfProjectId { + fmt.Fprintf(os.Stderr, "Error: File ID '%s' Not found. %v .", 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) + } + return body, err, true +} + // 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) + if body, _, chk := p.ChkBoundOrStandalone(p.FileID); chk { + json.Unmarshal(body, &p) + } else { + p.getBoundScriptInf(p.FileID) } - json.Unmarshal(body, &p) } else if len(p.WantName) > 0 { finf, err := p.nameToID(p.WantName) if err != nil { @@ -413,7 +456,7 @@ func (p *FileInf) scriptUploader(metadata map[string]interface{}, pr []byte) *Fi } body, err := r.FetchAPI() if err != nil { - fmt.Fprintf(os.Stderr, "Error: %v. ", err) + fmt.Fprintf(os.Stderr, "Error: %v.\n%v\n", err, string(body)) os.Exit(1) } var uf uploadedFile @@ -440,20 +483,22 @@ func (p *FileInf) fileUploader(metadata map[string]interface{}, file string) *Fi 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) + if file != "" { + 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{ @@ -466,7 +511,7 @@ func (p *FileInf) fileUploader(metadata map[string]interface{}, file string) *Fi } body, err := r.FetchAPI() if err != nil { - fmt.Fprintf(os.Stderr, "Error: %v. ", err) + fmt.Fprintf(os.Stderr, "Error: %v\n%v\n", err, string(body)) os.Exit(1) } var uf uploadedFile @@ -478,7 +523,9 @@ func (p *FileInf) fileUploader(metadata map[string]interface{}, file string) *Fi // 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 { + if c.String("projectname") == "" && len(p.UpFilename) == 0 { + p.Msgar = append(p.Msgar, "Error: No options. Please check HELP using 'ggsrun u --help'.") + } else if c.String("projectname") == "" && len(p.UpFilename) > 0 && p.ParentID == "" { for _, elm := range p.UpFilename { metadata := &fileUploaderMeta{ Name: filepath.Base(elm), @@ -520,49 +567,162 @@ func (p *FileInf) Uploader(c *cli.Context) *FileInf { } } } 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") + if p.ParentID == "" { + if p.ProjectType == "standalone" { + 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") + } + pre := p.createProject(c.String("timezone")) + _ = p.scriptUploader(u, pre) + p.createdprojectresult(len(p.UpFilename), metadata.Name) + } else { + parentId := p.createGoogleDocs(c) + p.createProjectInGoogleDocs(c, parentId) + } + } else { + if c.String("projectname") != "" { + p.createProjectInGoogleDocs(c, p.ParentID) + } else { + fmt.Fprintf(os.Stderr, "Error: No project name. Please input project name using '--projectname' or '-pn' and try again.\n") + os.Exit(1) + } } - var pr project + } + p.TotalEt = math.Trunc(time.Now().Sub(p.PstartTime).Seconds()*1000) / 1000 + return p +} + +// createProjectInGoogleDocs : Create new project as a bound script. +func (p *FileInf) createProjectInGoogleDocs(c *cli.Context, parentId string) { + metadata := &newProject{ + ParentId: parentId, + Title: c.String("projectname"), + } + meta, _ := json.Marshal(metadata) + asi := p.boundScriptCreator(meta) + manifests := p.getBoundScript(asi.ScriptId).getManifests(c.String("timezone")) + pre := p.createProjectForAppsScriptApi(asi.ScriptId).setManifests(manifests) + _ = p.ProjectUpdateByAppsScriptApi(pre) + p.createdprojectresult(len(p.UpFilename), metadata.Title) +} + +// createGoogleDocs : Create new Google Docs (spreadsheet, document, slide and form) +func (p *FileInf) createGoogleDocs(c *cli.Context) string { + metadata := &fileUploaderMeta{ + Name: func(c *cli.Context) string { + if c.String("googledocname") != "" { + return c.String("googledocname") + } + return c.String("projectname") + }(c), + Parents: func(folderId string) []string { + if folderId != "" { + return []string{folderId} + } + return []string{} + }(c.String("parentfolderid")), + MimeType: func(ptype string) string { + var ret string + switch strings.ToLower(ptype) { + case "spreadsheet": + ret = "application/vnd.google-apps.spreadsheet" + case "document": + ret = "application/vnd.google-apps.document" + case "slide": + ret = "application/vnd.google-apps.presentation" + case "form": + ret = "application/vnd.google-apps.form" + } + return ret + }(p.ProjectType), + } + upmeta, _ := json.Marshal(metadata) + var u map[string]interface{} + json.Unmarshal(upmeta, &u) + p.fileUploader(u, "") + return p.UppedFiles[0].ID +} + +// createdprojectresult : Result of created project +func (p *FileInf) createdprojectresult(num int, filename string) { + if num > 0 { + p.Msgar = append(p.Msgar, fmt.Sprintf("Uploaded %d scripts as new project with a name of '%s'.", num, filename)) + } else { + p.Msgar = append(p.Msgar, fmt.Sprintf("New project was created as the filename of '%s'.", filename)) + } +} + +// createProject : Create new project as json +func (p *FileInf) createProject(timeZone string) []byte { + var pr project + if len(p.UpFilename) > 0 { 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" || - filepath.Ext(elm) == ".json" { + if ChkExtention(filepath.Ext(elm)) { 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" - case ".json": - scripttype = "json" - } - return scripttype - }(filepath.Ext(elm)), + Name: strings.Replace(filepath.Base(elm), filepath.Ext(elm), "", -1), + Type: ExtToType(filepath.Ext(elm), false), 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) + if len(pr.Files) == 0 { + fmt.Fprintf(os.Stderr, "Error: Inputted files cannot be used for GAS project.\n") + os.Exit(1) + } + } else { + filedata := &filea{ + Name: "Code", + Type: "server_js", + Source: "function myFunction() {\n \n}\n", + } + pr.Files = append(pr.Files, *filedata) } - return p + if timeZone != "" { + filedata := &filea{ + Name: "appsscript", + Type: "json", + Source: "{\n \"timeZone\": \"" + timeZone + "\",\n \"dependencies\": {\n },\n \"exceptionLogging\": \"STACKDRIVER\"\n}\n", + } + pr.Files = append(pr.Files, *filedata) + } + pre, _ := json.Marshal(pr) + return pre +} + +// ChkExtention : Check extension of inputted files. +func ChkExtention(ex string) bool { + switch strings.ToLower(ex) { + case ".gs", ".gas", ".js", ".htm", ".html", ".json": + return true + default: + return false + } +} + +// ExtToType : Convert extension to scripttype for project. +func ExtToType(ex string, uppercase bool) string { + var scripttype string + switch strings.ToLower(ex) { + case ".gs", ".gas", ".js": + scripttype = "server_js" + case ".htm", ".html": + scripttype = "html" + case ".json": + scripttype = "json" + } + if uppercase { + scripttype = strings.ToUpper(scripttype) + } + return scripttype } // extToGMime : Convert from extension to mimeType of the files on Google. @@ -607,7 +767,7 @@ func (p *FileInf) GetFileList(c *cli.Context) *FileInf { } os.Exit(1) } else { - fmt.Fprintf(os.Stderr, "Error: File name '%s' is not found. ", p.SearchByName) + fmt.Fprintf(os.Stderr, "Error: File name '%s' is not found. How about trying this using file ID, again?", p.SearchByName) os.Exit(1) } p.TotalEt = math.Trunc(time.Now().Sub(p.PstartTime).Seconds()*1000) / 1000 @@ -615,12 +775,11 @@ func (p *FileInf) GetFileList(c *cli.Context) *FileInf { } 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) + if body, _, chk := p.ChkBoundOrStandalone(p.SearchByID); chk { + json.Unmarshal(body, &p) + } else { + p.getBoundScriptInf(p.SearchByID) } - json.Unmarshal(body, &p) p.TotalEt = math.Trunc(time.Now().Sub(p.PstartTime).Seconds()*1000) / 1000 return p }