├── pics ├── select_create.png └── test_as_addon.png ├── CHANGELOG.md ├── .gitignore ├── CONTRIBUTING.md ├── pubspec.yaml ├── bin ├── upload.dart ├── main.dart ├── gsify.dart └── run.dart ├── lib ├── run.dart ├── main.dart ├── gsify.dart ├── src │ ├── api_client.dart │ └── preamble.dart └── upload.dart ├── example └── example.dart ├── LICENSE └── README.md /pics/select_create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/apps_script_tools/HEAD/pics/select_create.png -------------------------------------------------------------------------------- /pics/test_as_addon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/apps_script_tools/HEAD/pics/test_as_addon.png -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.0.4 4 | - Update args dependency, and reformat one file. 5 | 6 | ## 1.0.3 7 | - Really fix tool when there are files with the same name. 8 | 9 | ## 1.0.2 10 | - Fix tool when there are files with the same name. 11 | 12 | ## 1.0.1 13 | - Fix dartfmt. 14 | 15 | ## 1.0.0 16 | - Change version. 17 | - Cleanups to make pana happy. 18 | - Works better now if there exist files with the same name. 19 | 20 | ## 0.0.1+1 21 | 22 | - Some fixes in README. 23 | 24 | ## 0.0.1 25 | 26 | - Initial version. 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://www.dartlang.org/guides/libraries/private-files 2 | 3 | # Files and directories created by pub 4 | .dart_tool/ 5 | .packages 6 | build/ 7 | # If you're building an application, you may want to check-in your pubspec.lock 8 | pubspec.lock 9 | 10 | # Directory created by dartdoc 11 | # If you don't generate documentation locally you can remove this line. 12 | doc/api/ 13 | 14 | # Avoid committing generated Javascript files: 15 | *.dart.js 16 | *.info.json # Produced by the --dump-info flag. 17 | *.js # When generated by dart2js. Don't specify *.js if your 18 | # project includes source files written in JavaScript. 19 | *.js_ 20 | *.js.deps 21 | *.js.map 22 | 23 | .idea 24 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution, 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https:#www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: apps_script_tools 16 | description: Tools for using Dart programs as Google Apps scripts on the server. 17 | version: 1.0.4 18 | author: Florian Loitsch 19 | homepage: https://github.com/google/apps_script_tools 20 | 21 | environment: 22 | sdk: ">=2.0.0 <3.0.0" 23 | 24 | dependencies: 25 | args: ^1.5.0 26 | googleapis: ^0.54.0 27 | googleapis_auth: ^0.2.3 28 | path: ^1.5.1 29 | watcher: ^0.9.7 30 | http: ^0.12.0 31 | 32 | executables: 33 | apps_script_watch: main 34 | apps_script_gsify: gsify 35 | apps_script_upload: upload 36 | -------------------------------------------------------------------------------- /bin/upload.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:args/args.dart'; 16 | import 'package:apps_script_tools/upload.dart'; 17 | 18 | void help(ArgParser parser) { 19 | print("Uploads a given '.gs' script to Google Drive as a Google Apps script"); 20 | print("Usage: upload compiled.gs destination"); 21 | print(parser.usage); 22 | } 23 | 24 | main(List args) async { 25 | var parser = ArgParser(); 26 | parser.addFlag("help", abbr: "h", help: "this help", negatable: false); 27 | var parsedArgs = parser.parse(args); 28 | if (parsedArgs['help'] || parsedArgs.rest.length != 2) { 29 | help(parser); 30 | return parsedArgs['help'] ? 0 : 1; 31 | } 32 | 33 | var sourcePath = parsedArgs.rest.first; 34 | var destination = parsedArgs.rest.last; 35 | await upload(sourcePath, destination); 36 | } 37 | -------------------------------------------------------------------------------- /lib/run.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:async'; 16 | import 'dart:convert' show json; 17 | 18 | import 'package:googleapis/script/v1.dart'; 19 | 20 | import 'src/api_client.dart'; 21 | 22 | dynamic _convertArg(String arg) { 23 | try { 24 | return json.decode(arg); 25 | } catch (e) { 26 | return arg; 27 | } 28 | } 29 | 30 | Future runScript(String scriptId, String funName, String clientId, 31 | String clientSecret, List scopes, List unconvertedArgs, 32 | {bool devMode = false, String authCachePath}) async { 33 | var apiClient = ApiClient(); 34 | 35 | await apiClient.authenticate(clientId, clientSecret, scopes, authCachePath); 36 | 37 | List convertedArgs = unconvertedArgs.map(_convertArg).toList(); 38 | 39 | var api = ScriptApi(apiClient.client); 40 | 41 | var request = ExecutionRequest() 42 | ..devMode = devMode 43 | ..function = funName 44 | ..parameters = convertedArgs; 45 | var operation = await api.scripts.run(request, scriptId); 46 | print(operation.response); 47 | } 48 | -------------------------------------------------------------------------------- /example/example.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Copyright 2018 Google LLC 16 | // 17 | // Licensed under the Apache License, Version 2.0 (the "License"); 18 | // you may not use this file except in compliance with the License. 19 | // You may obtain a copy of the License at 20 | // 21 | // https://www.apache.org/licenses/LICENSE-2.0 22 | // 23 | // Unless required by applicable law or agreed to in writing, software 24 | // distributed under the License is distributed on an "AS IS" BASIS, 25 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 26 | // See the License for the specific language governing permissions and 27 | // limitations under the License. 28 | 29 | import 'package:apps_script_tools/upload.dart'; 30 | 31 | // A sample script that uploads a dart2js-compiled file to the cloud as 32 | // google-apps script. 33 | // 34 | // Generally, one should simply use the provided programs, but pub wants to 35 | // have an example, and otherwise decreases the score and thus relevancy. 36 | main(List args) async { 37 | var sourcePath = args[0]; 38 | var destination = args[1]; 39 | upload(sourcePath, destination); 40 | } 41 | -------------------------------------------------------------------------------- /bin/main.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:args/args.dart'; 16 | import 'package:apps_script_tools/main.dart'; 17 | 18 | void help(ArgParser parser) { 19 | print("Watches the source-javaScript file and uploads it automatically as a"); 20 | print("Google Apps Script whenever it changes."); 21 | print(""); 22 | print(parser.usage); 23 | } 24 | 25 | main(List args) async { 26 | var parser = ArgParser(); 27 | parser.addMultiOption("stub", abbr: 's', help: "provides a function stub"); 28 | parser.addFlag("only-current-document", 29 | help: "only accesses the current document " 30 | "(https://developers.google.com/apps-script/" 31 | "guides/services/authorization)", 32 | negatable: false); 33 | parser.addFlag("not-only-current-document", 34 | help: "force multi-document access", negatable: false); 35 | parser.addFlag("help", abbr: "h", help: "this help", negatable: false); 36 | var parsedArgs = parser.parse(args); 37 | if (parsedArgs['help'] || parsedArgs.rest.length != 2) { 38 | help(parser); 39 | return parsedArgs['help'] ? 0 : 1; 40 | } 41 | 42 | var sourcePath = parsedArgs.rest.first; 43 | var destination = parsedArgs.rest.last; 44 | List interfaceFunctions = parsedArgs['stub']; 45 | bool onlyCurrentDocument = parsedArgs['only-current-document']; 46 | bool notOnlyCurrentDocument = parsedArgs['not-only-current-document']; 47 | 48 | await startWatching(sourcePath, destination, 49 | interfaceFunctions: interfaceFunctions, 50 | onlyCurrentDocument: onlyCurrentDocument, 51 | notOnlyCurrentDocument: notOnlyCurrentDocument); 52 | } 53 | -------------------------------------------------------------------------------- /bin/gsify.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:args/args.dart'; 16 | import 'package:apps_script_tools/gsify.dart'; 17 | 18 | void help(ArgParser parser) { 19 | print("Converts a dart2js compiled output file into a valid" 20 | " Google Apps Script."); 21 | print(""); 22 | print(parser.usage); 23 | } 24 | 25 | main(args) { 26 | var parser = ArgParser(); 27 | parser.addMultiOption("stub", abbr: 's', help: "provides a function stub"); 28 | parser.addFlag("only-current-document", 29 | help: "only accesses the current document " 30 | "(https://developers.google.com/apps-script/" 31 | "guides/services/authorization)", 32 | negatable: false); 33 | parser.addFlag("not-only-current-document", 34 | help: "force multi-document access", negatable: false); 35 | parser.addOption("out", abbr: "o", help: "path of generated gs script"); 36 | parser.addFlag("help", abbr: "h", help: "this help", negatable: false); 37 | var parsedArgs = parser.parse(args); 38 | if (parsedArgs['out'] == null || 39 | parsedArgs['help'] || 40 | parsedArgs.rest.length != 1) { 41 | help(parser); 42 | return parsedArgs['help'] ? 0 : 1; 43 | } 44 | 45 | var sourcePath = parsedArgs.rest.first; 46 | String outPath = parsedArgs['out']; 47 | List interfaceFunctions = parsedArgs['stub']; 48 | bool onlyCurrentDocument = parsedArgs['only-current-document']; 49 | bool notOnlyCurrentDocument = parsedArgs['not-only-current-document']; 50 | 51 | gsifyFile(sourcePath, outPath, 52 | interfaceFunctions: interfaceFunctions, 53 | onlyCurrentDocument: onlyCurrentDocument, 54 | notOnlyCurrentDocument: notOnlyCurrentDocument); 55 | } 56 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:io' as io; 16 | import 'dart:async'; 17 | import 'package:watcher/watcher.dart'; 18 | import 'package:path/path.dart' as p; 19 | import 'gsify.dart'; 20 | import 'upload.dart'; 21 | 22 | /// Watches the given [path] and emits an event everytime it changes. 23 | /// 24 | /// When [emitAtListen] is true, also emits an event when this function is 25 | /// started and the file already exists. 26 | Stream watchPath(String path, {bool emitAtListen = false}) async* { 27 | var file = io.File(path); 28 | if (file.existsSync() && emitAtListen) yield null; 29 | 30 | outerLoop: 31 | while (true) { 32 | if (!file.existsSync()) { 33 | var directory = p.dirname(path); 34 | while (!io.Directory(directory).existsSync()) { 35 | directory = p.dirname(directory); 36 | } 37 | await for (var _ in DirectoryWatcher(directory).events) { 38 | if (file.existsSync()) { 39 | yield null; 40 | break; 41 | } else { 42 | // In case we are listening for directories to be created. 43 | continue outerLoop; 44 | } 45 | } 46 | } 47 | if (file.existsSync()) { 48 | await for (var event in FileWatcher(path).events) { 49 | if (event.type != ChangeType.REMOVE) yield null; 50 | } 51 | } 52 | } 53 | } 54 | 55 | /// Watches the given [sourcePath] and uploads it to [destination] whenever it 56 | /// changes. 57 | /// 58 | /// When gsifying the input source uses [interfaceFunctions], 59 | /// [onlyCurrentDocument] and [notOnlyCurrentDocument] to, optionally, add 60 | /// some boilerplate. See [gsify] for more information. 61 | Future startWatching(String sourcePath, String destination, 62 | {List interfaceFunctions, 63 | bool onlyCurrentDocument, 64 | bool notOnlyCurrentDocument}) async { 65 | var uploader = Uploader(destination); 66 | await uploader.authenticate(); 67 | 68 | await for (var _ in watchPath(sourcePath, emitAtListen: true)) { 69 | var source = io.File(sourcePath).readAsStringSync(); 70 | var gsified = gsify(source, 71 | interfaceFunctions: interfaceFunctions, 72 | onlyCurrentDocument: onlyCurrentDocument, 73 | notOnlyCurrentDocument: notOnlyCurrentDocument); 74 | await uploader.uploadScript(gsified); 75 | } 76 | await uploader.close(); 77 | } 78 | -------------------------------------------------------------------------------- /lib/gsify.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:io'; 16 | import 'src/preamble.dart'; 17 | 18 | /// Takes a dart2js compiled code [source] and converts it into a valid 19 | /// Google Apps Script. 20 | /// 21 | /// Always includes a necessary preamble. 22 | /// 23 | /// Optionally includes function stubs for functions listed in 24 | /// [interfaceFunctions]. These stubs can be used as entry points (which 25 | /// must be statically visible), but can be overwritten from withing the 26 | /// Dart code (before they are executed). 27 | /// 28 | /// When [onlyCurrentDocument] is true, adds a `/* @OnlyCurrentDoc */` comment. 29 | /// When [notOnlyCurrentDocument] is true, adds a `/* @NotOnlyCurrentDoc */` 30 | /// comment. It doesn't make sense to set both booleans to true. 31 | String gsify(String source, 32 | {List interfaceFunctions = const [], 33 | bool onlyCurrentDocument = false, 34 | bool notOnlyCurrentDocument = false}) { 35 | var result = StringBuffer(); 36 | if (onlyCurrentDocument) { 37 | result.writeln("/* @OnlyCurrentDoc */"); 38 | } 39 | if (notOnlyCurrentDocument) { 40 | result.writeln("/* @NotOnlyCurrentDoc */"); 41 | } 42 | for (var fun in interfaceFunctions) { 43 | // These functions can be overridden by the Dart program. 44 | result.writeln("function $fun() {}"); 45 | } 46 | result.writeln(PREAMBLE); 47 | result.write(source); 48 | return result.toString(); 49 | } 50 | 51 | /// Takes a dart2js compiled output file [sourcePath] and converts it into a 52 | /// valid Google Apps Script writing it into [outPath]. 53 | /// 54 | /// Always includes a necessary preamble. 55 | /// 56 | /// Optionally includes function stubs for functions listed in 57 | /// [interfaceFunctions]. These stubs can be used as entry points (which 58 | /// must be statically visible), but can be overwritten from withing the 59 | /// Dart code (before they are executed). 60 | /// 61 | /// When [onlyCurrentDocument] is true, adds a `/* @OnlyCurrentDoc */` comment. 62 | /// When [notOnlyCurrentDocument] is true, adds a `/* @NotOnlyCurrentDoc */` 63 | /// comment. It doesn't make sense to set both booleans to true. 64 | void gsifyFile(String sourcePath, String outPath, 65 | {List interfaceFunctions = const [], 66 | bool onlyCurrentDocument = false, 67 | bool notOnlyCurrentDocument = false}) { 68 | var source = File(sourcePath).readAsStringSync(); 69 | var gsified = gsify(source, 70 | interfaceFunctions: interfaceFunctions, 71 | onlyCurrentDocument: onlyCurrentDocument, 72 | notOnlyCurrentDocument: notOnlyCurrentDocument); 73 | File(outPath).writeAsStringSync(gsified); 74 | } 75 | -------------------------------------------------------------------------------- /bin/run.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'package:args/args.dart'; 16 | import 'package:apps_script_tools/run.dart'; 17 | 18 | const scriptId = "M3QyfgU45qAOUdM-H2VBGr4gwnYFpKilg"; 19 | 20 | void help(ArgParser parser) { 21 | print(""" 22 | Runs the given function in the script 23 | run [--dev-mode] [scopes*] --client-id --client-secret scriptId function-name [args*] 24 | 25 | The scriptId can be found in the Project Properties. 26 | From the Script Editor: 27 | -> File 28 | -> Project Properties 29 | -> Info Tab. 30 | 31 | The scopes can be found in the Project Properties. 32 | From the Script Editor: 33 | -> File 34 | -> Project Properties 35 | -> Scopes Tab 36 | 37 | The client-id and client-secret are OAuth ids for the script's project. 38 | They need to be created for each script (or the script must be moved into a 39 | project that already has one). 40 | 41 | From the Script Editor: 42 | -> Resources 43 | -> Cloud Platform project 44 | -> 45 | -> Getting started: Enable APIs and get credentials such as keys 46 | -> Credentials 47 | If necessary create a new ("Other") OAuth client ID. 48 | This page can also be accessed by https://console.cloud.google.com/apis/credentials?project=PROJECT_ID 49 | where PROJECT_ID is the ID from step 3. 50 | 51 | The function-name is the entry-point of the function. 52 | 53 | The script generally does not cache the authentication since the scopes may 54 | change for different scripts or versions. Users can provide an explicit 55 | auth-cache path where the authentication is stored. 56 | 57 | Each argument to the Apps Script is parsed as JSON, and if that fails, passed 58 | verbatim as a string. 59 | 60 | Example: 61 | run --dev-mode -s https://www.googleapis.com/auth/documents helloWorld 62 | """); 63 | print(parser.usage); 64 | } 65 | 66 | main(List args) async { 67 | var parser = ArgParser(); 68 | parser.addFlag("dev-mode", 69 | help: "Runs the most recently saved version rather than the deployed " 70 | "version."); 71 | parser.addMultiOption("scope", abbr: "s"); 72 | parser.addOption("auth-cache", 73 | help: "The file-path where the authentication should be cached"); 74 | parser.addFlag("help", abbr: "h", help: "this help", negatable: false); 75 | parser.addOption( 76 | "client-id", 77 | help: "the client id", 78 | ); 79 | parser.addOption("client-secret", help: "the client secret"); 80 | 81 | var parsedArgs = parser.parse(args); 82 | if (parsedArgs['help'] || 83 | parsedArgs["client-id"] == null || 84 | parsedArgs["client-secret"] == null || 85 | parsedArgs.rest.length < 2) { 86 | help(parser); 87 | return parsedArgs['help'] ? 0 : 1; 88 | } 89 | 90 | bool devMode = parsedArgs['dev-mode']; 91 | 92 | String scriptId = parsedArgs.rest[0]; 93 | String scriptFun = parsedArgs.rest[1]; 94 | 95 | String clientId = parsedArgs["client-id"]; 96 | String clientSecret = parsedArgs["client-secret"]; 97 | 98 | List scopes = parsedArgs['scope']; 99 | 100 | String authCachePath = parsedArgs['auth-cache']; 101 | 102 | await runScript(scriptId, scriptFun, clientId, clientSecret, scopes, 103 | parsedArgs.rest.skip(2).toList(), 104 | devMode: devMode, authCachePath: authCachePath); 105 | } 106 | -------------------------------------------------------------------------------- /lib/src/api_client.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:io' as io; 16 | import 'dart:async'; 17 | import 'dart:convert'; 18 | import 'package:googleapis_auth/auth_io.dart'; 19 | import 'package:path/path.dart' as p; 20 | import 'package:http/http.dart' show Client; 21 | 22 | /// An Google API Client. 23 | /// 24 | /// Asks the user to authenticate and caches the tokens. 25 | class ApiClient { 26 | Client _baseClient; 27 | AuthClient client; 28 | 29 | /// Authenticates this uploader. 30 | /// 31 | /// Uses Google APIs to authenticate with [id] and [secret]. If id 32 | Future authenticate(String id, String secret, List scopes, 33 | String authCachePath) async { 34 | _baseClient = Client(); 35 | var clientId = ClientId(id, secret); 36 | // TODO(floitsch): this probably doesn't work when the scopes change. 37 | var credentials = _readSavedCredentials(authCachePath); 38 | if (credentials == null || 39 | credentials.refreshToken == null && 40 | credentials.accessToken.hasExpired) { 41 | credentials = await obtainAccessCredentialsViaUserConsent( 42 | clientId, scopes, _baseClient, (String str) { 43 | print("Please authorize at this URL: $str"); 44 | }); 45 | _saveCredentials(authCachePath, credentials); 46 | } 47 | client = credentials.refreshToken == null 48 | ? authenticatedClient(_baseClient, credentials) 49 | : autoRefreshingClient(clientId, credentials, _baseClient); 50 | } 51 | 52 | /// Shuts down this uploader. 53 | Future close() async { 54 | await client.close(); 55 | await _baseClient.close(); 56 | } 57 | 58 | AccessCredentials _readSavedCredentials(String savedCredentialsPath) { 59 | if (savedCredentialsPath == null) return null; 60 | 61 | var file = io.File(savedCredentialsPath); 62 | if (!file.existsSync()) return null; 63 | var decoded = json.decode(file.readAsStringSync()); 64 | var refreshToken = decoded['refreshToken']; 65 | if (refreshToken == null) { 66 | print("refreshToken missing. Users will have to authenticate again."); 67 | } 68 | var jsonAccessToken = decoded['accessToken']; 69 | var accessToken = AccessToken( 70 | jsonAccessToken['type'], 71 | jsonAccessToken['data'], 72 | DateTime.fromMillisecondsSinceEpoch(jsonAccessToken['expiry'], 73 | isUtc: true)); 74 | var scopes = (decoded['scopes'] as List).cast(); 75 | return AccessCredentials(accessToken, refreshToken, scopes); 76 | } 77 | 78 | void _saveCredentials( 79 | String savedCredentialsPath, AccessCredentials credentials) { 80 | if (savedCredentialsPath == null) return; 81 | 82 | try { 83 | var accessToken = credentials.accessToken; 84 | var encoded = json.encode({ 85 | 'refreshToken': credentials.refreshToken, 86 | 'accessToken': { 87 | "type": accessToken.type, 88 | "data": accessToken.data, 89 | "expiry": accessToken.expiry.millisecondsSinceEpoch 90 | }, 91 | 'scopes': credentials.scopes 92 | }); 93 | var directory = io.Directory(p.dirname(savedCredentialsPath)); 94 | if (!directory.existsSync()) directory.createSync(recursive: true); 95 | io.File(savedCredentialsPath).writeAsStringSync(encoded); 96 | } catch (e) { 97 | print("Couldn't save credentials: $e"); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/upload.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import 'dart:io' as io; 16 | import 'dart:async'; 17 | import 'dart:convert'; 18 | import 'package:googleapis/drive/v3.dart'; 19 | import 'package:path/path.dart' as p; 20 | 21 | import 'src/api_client.dart'; 22 | 23 | const List _SCOPES = [DriveApi.DriveScope, DriveApi.DriveScriptsScope]; 24 | 25 | const String _SCRIPT_MIME_TYPE = "application/vnd.google-apps.script"; 26 | const String _CONTENT_TYPE = "application/vnd.google-apps.script+json"; 27 | 28 | const String apiId = 29 | "182739467893-iq44a0gc3h2easrua3mru8n84pdvpi4h.apps.googleusercontent.com"; 30 | const String apiSecret = "SNmwenVx4fd5aE7aeEixPoxI"; 31 | 32 | String get _savedCredentialsPath { 33 | String fileName = "auth.json"; 34 | if (io.Platform.environment.containsKey('APPS_SCRIPT_TOOLS_CACHE')) { 35 | return io.Platform.environment['APPS_SCRIPT_TOOLS_CACHE']; 36 | } else if (io.Platform.operatingSystem == 'windows') { 37 | var appData = io.Platform.environment['APPDATA']; 38 | return p.join(appData, 'AppsScriptTools', 'Cache', fileName); 39 | } else { 40 | return p.join( 41 | io.Platform.environment['HOME'], '.apps_script_tools-cache', fileName); 42 | } 43 | } 44 | 45 | /// A Script Uploader. 46 | /// 47 | /// Once authenticated, the uploader can upload new versions of the script. 48 | class Uploader { 49 | final ApiClient _apiClient = ApiClient(); 50 | final String _destination; 51 | DriveApi _drive; 52 | 53 | String _projectName; 54 | String _destinationFolderId; 55 | 56 | /// Instantiates an uploader with the provided Google Drive destination. 57 | Uploader(this._destination); 58 | 59 | /// Authenticates this uploader. 60 | /// 61 | /// Uses Google APIs to authenticate with [id] and [secret]. If id 62 | Future authenticate() async { 63 | await _apiClient.authenticate( 64 | apiId, apiSecret, _SCOPES, _savedCredentialsPath); 65 | _drive = DriveApi(_apiClient.client); 66 | } 67 | 68 | /// Shuts down this uploader. 69 | Future close() async { 70 | await _apiClient.close(); 71 | } 72 | 73 | String _createPayload( 74 | String source, String projectName, Map existing) { 75 | // See https://developers.google.com/apps-script/guides/import-export. 76 | var payload = { 77 | "name": projectName, 78 | "type": "server_js", 79 | "source": source, 80 | }; 81 | if (existing != null) { 82 | payload["id"] = existing["files"][0]["id"]; 83 | } 84 | return json.encode({ 85 | "files": [payload] 86 | }); 87 | } 88 | 89 | Future _findFolder(DriveApi drive, Iterable segments) async { 90 | var parentId = "root"; 91 | for (var segment in segments) { 92 | var q = 93 | "name = '$segment' and '$parentId' in parents and trashed = false"; 94 | var nestedFiles = (await drive.files.list(q: q)).files; 95 | var folders = nestedFiles 96 | .where( 97 | (file) => file.mimeType == "application/vnd.google-apps.folder") 98 | .toList(); 99 | if (folders.length == 1) { 100 | parentId = folders.first.id; 101 | } else if (folders.isEmpty) { 102 | throw "Couldn't find folder $segment"; 103 | } else { 104 | throw "Couldn't find single folder $segment"; 105 | } 106 | } 107 | return parentId; 108 | } 109 | 110 | /// Uploads the given [source] to the location provided at construction. 111 | /// 112 | /// If the script already exists replaces the unique source file within the 113 | /// script with the provided source. 114 | Future uploadScript(String source) async { 115 | if (_projectName == null) { 116 | var segments = _destination.split("/"); 117 | var folderSegments = segments.take(segments.length - 1); 118 | _projectName = segments.last; 119 | _destinationFolderId = await _findFolder(_drive, folderSegments); 120 | } 121 | 122 | var query = "name = '$_projectName' and " 123 | "'$_destinationFolderId' in parents and " 124 | "trashed = false"; 125 | var sameNamedFiles = (await _drive.files.list(q: query)).files; 126 | var scripts = sameNamedFiles 127 | .where((file) => file.mimeType == "application/vnd.google-apps.script") 128 | .toList(); 129 | var existing; 130 | if (scripts.isEmpty) { 131 | print("Need to create new project."); 132 | } else if (scripts.length == 1) { 133 | print("Need to update existing project."); 134 | try { 135 | Media media = await _drive.files.export(scripts[0].id, _CONTENT_TYPE, 136 | downloadOptions: DownloadOptions.FullMedia); 137 | existing = await media.stream 138 | .transform(utf8.decoder) 139 | .transform(json.decoder) 140 | .first; 141 | } catch (e) { 142 | print(e); 143 | rethrow; 144 | } 145 | } else { 146 | print("Multiple scripts of same name. Don't know which one to update."); 147 | return; 148 | } 149 | 150 | var file = File() 151 | ..name = _projectName 152 | ..mimeType = _SCRIPT_MIME_TYPE; 153 | 154 | var payload = _createPayload(source, _projectName, existing); 155 | var utf8Encoded = utf8.encode(payload); 156 | var media = Media( 157 | Stream>.fromIterable([utf8Encoded]), utf8Encoded.length, 158 | contentType: _CONTENT_TYPE); 159 | 160 | if (scripts.isEmpty) { 161 | print("Creating new file ${_projectName}"); 162 | file.parents = [_destinationFolderId]; 163 | await _drive.files.create(file, uploadMedia: media); 164 | } else if (scripts.length == 1) { 165 | // Update the existing file. 166 | print("Updating existing file ${_projectName}"); 167 | await _drive.files.update(file, scripts[0].id, uploadMedia: media); 168 | } 169 | print("Uploading ${_projectName} done"); 170 | } 171 | } 172 | 173 | /// Uploads the given [sourcePath] to the [destination] in Google Drive. 174 | /// 175 | Future upload(String sourcePath, String destination) async { 176 | var uploader = Uploader(destination); 177 | await uploader.authenticate(); 178 | var source = io.File(sourcePath).readAsStringSync(); 179 | await uploader.uploadScript(source); 180 | await uploader.close(); 181 | } 182 | -------------------------------------------------------------------------------- /lib/src/preamble.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const String PREAMBLE = r""" 16 | function dartPrint(message) { 17 | Logger.log(message); 18 | } 19 | 20 | var self = this; 21 | 22 | (function(self) { 23 | // Using strict mode to avoid accidentally defining global variables. 24 | "use strict"; // Should be first statement of this function. 25 | 26 | // Event loop. 27 | 28 | // Task queue as cyclic list queue. 29 | var taskQueue = new Array(8); // Length is power of 2. 30 | var head = 0; 31 | var tail = 0; 32 | var mask = taskQueue.length - 1; 33 | function addTask(elem) { 34 | taskQueue[head] = elem; 35 | head = (head + 1) & mask; 36 | if (head == tail) _growTaskQueue(); 37 | } 38 | function removeTask() { 39 | if (head == tail) return; 40 | var result = taskQueue[tail]; 41 | taskQueue[tail] = undefined; 42 | tail = (tail + 1) & mask; 43 | return result; 44 | } 45 | function _growTaskQueue() { 46 | // head == tail. 47 | var length = taskQueue.length; 48 | var split = head; 49 | taskQueue.length = length * 2; 50 | if (split * 2 < length) { // split < length / 2 51 | for (var i = 0; i < split; i++) { 52 | taskQueue[length + i] = taskQueue[i]; 53 | taskQueue[i] = undefined; 54 | } 55 | head += length; 56 | } else { 57 | for (var i = split; i < length; i++) { 58 | taskQueue[length + i] = taskQueue[i]; 59 | taskQueue[i] = undefined; 60 | } 61 | tail += length; 62 | } 63 | mask = taskQueue.length - 1; 64 | } 65 | 66 | // Mapping from timer id to timer function. 67 | // The timer id is written on the function as .$timerId. 68 | // That field is cleared when the timer is cancelled, but it is not returned 69 | // from the queue until its time comes. 70 | var timerIds = {}; 71 | var timerIdCounter = 1; // Counter used to assign ids. 72 | 73 | // Zero-timer queue as simple array queue using push/shift. 74 | var zeroTimerQueue = []; 75 | 76 | function addTimer(f, ms) { 77 | var id = timerIdCounter++; 78 | f.$timerId = id; 79 | timerIds[id] = f; 80 | if (ms == 0) { 81 | zeroTimerQueue.push(f); 82 | } else { 83 | addDelayedTimer(f, ms); 84 | } 85 | return id; 86 | } 87 | 88 | function nextZeroTimer() { 89 | while (zeroTimerQueue.length > 0) { 90 | var action = zeroTimerQueue.shift(); 91 | if (action.$timerId !== undefined) return action; 92 | } 93 | } 94 | 95 | function nextEvent() { 96 | var action = removeTask(); 97 | if (action) { 98 | return action; 99 | } 100 | do { 101 | action = nextZeroTimer(); 102 | if (action) break; 103 | var nextList = nextDelayedTimerQueue(); 104 | if (!nextList) { 105 | return; 106 | } 107 | var newTime = nextList.shift(); 108 | advanceTimeTo(newTime); 109 | zeroTimerQueue = nextList; 110 | } while (true) 111 | var id = action.$timerId; 112 | clearTimerId(action, id); 113 | return action; 114 | } 115 | 116 | // Mocking time. 117 | var timeOffset = 0; 118 | var now = function() { 119 | // Install the mock Date object only once. 120 | // Following calls to "now" will just use the new (mocked) Date.now 121 | // method directly. 122 | installMockDate(); 123 | now = Date.now; 124 | return Date.now(); 125 | }; 126 | var originalDate = Date; 127 | var originalNow = originalDate.now; 128 | function advanceTimeTo(time) { 129 | timeOffset = time - originalNow(); 130 | } 131 | function installMockDate() { 132 | var NewDate = function Date(Y, M, D, h, m, s, ms) { 133 | if (this instanceof Date) { 134 | // Assume a construct call. 135 | switch (arguments.length) { 136 | case 0: return new originalDate(originalNow() + timeOffset); 137 | case 1: return new originalDate(Y); 138 | case 2: return new originalDate(Y, M); 139 | case 3: return new originalDate(Y, M, D); 140 | case 4: return new originalDate(Y, M, D, h); 141 | case 5: return new originalDate(Y, M, D, h, m); 142 | case 6: return new originalDate(Y, M, D, h, m, s); 143 | default: return new originalDate(Y, M, D, h, m, s, ms); 144 | } 145 | } 146 | return new originalDate(originalNow() + timeOffset).toString(); 147 | }; 148 | NewDate.UTC = originalDate.UTC; 149 | NewDate.parse = originalDate.parse; 150 | NewDate.now = function now() { return originalNow() + timeOffset; }; 151 | NewDate.prototype = originalDate.prototype; 152 | originalDate.prototype.constructor = NewDate; 153 | Date = NewDate; 154 | } 155 | 156 | // Heap priority queue with key index. 157 | // Each entry is list of [timeout, callback1 ... callbackn]. 158 | var timerHeap = []; 159 | var timerIndex = {}; 160 | function addDelayedTimer(f, ms) { 161 | var timeout = now() + ms; 162 | var timerList = timerIndex[timeout]; 163 | if (timerList == null) { 164 | timerList = [timeout, f]; 165 | timerIndex[timeout] = timerList; 166 | var index = timerHeap.length; 167 | timerHeap.length += 1; 168 | bubbleUp(index, timeout, timerList); 169 | } else { 170 | timerList.push(f); 171 | } 172 | } 173 | 174 | function nextDelayedTimerQueue() { 175 | if (timerHeap.length == 0) return null; 176 | var result = timerHeap[0]; 177 | var last = timerHeap.pop(); 178 | if (timerHeap.length > 0) { 179 | bubbleDown(0, last[0], last); 180 | } 181 | return result; 182 | } 183 | 184 | function bubbleUp(index, key, value) { 185 | while (index != 0) { 186 | var parentIndex = (index - 1) >> 1; 187 | var parent = timerHeap[parentIndex]; 188 | var parentKey = parent[0]; 189 | if (key > parentKey) break; 190 | timerHeap[index] = parent; 191 | index = parentIndex; 192 | } 193 | timerHeap[index] = value; 194 | } 195 | 196 | function bubbleDown(index, key, value) { 197 | while (true) { 198 | var leftChildIndex = index * 2 + 1; 199 | if (leftChildIndex >= timerHeap.length) break; 200 | var minChildIndex = leftChildIndex; 201 | var minChild = timerHeap[leftChildIndex]; 202 | var minChildKey = minChild[0]; 203 | var rightChildIndex = leftChildIndex + 1; 204 | if (rightChildIndex < timerHeap.length) { 205 | var rightChild = timerHeap[rightChildIndex]; 206 | var rightKey = rightChild[0]; 207 | if (rightKey < minChildKey) { 208 | minChildIndex = rightChildIndex; 209 | minChild = rightChild; 210 | minChildKey = rightKey; 211 | } 212 | } 213 | if (minChildKey > key) break; 214 | timerHeap[index] = minChild; 215 | index = minChildIndex; 216 | } 217 | timerHeap[index] = value; 218 | } 219 | 220 | function addInterval(f, ms) { 221 | var id = timerIdCounter++; 222 | function repeat() { 223 | // Reactivate with the same id. 224 | repeat.$timerId = id; 225 | timerIds[id] = repeat; 226 | addDelayedTimer(repeat, ms); 227 | f(); 228 | } 229 | repeat.$timerId = id; 230 | timerIds[id] = repeat; 231 | addDelayedTimer(repeat, ms); 232 | return id; 233 | } 234 | 235 | function cancelTimer(id) { 236 | var f = timerIds[id]; 237 | if (f == null) return; 238 | clearTimerId(f, id); 239 | } 240 | 241 | function clearTimerId(f, id) { 242 | f.$timerId = undefined; 243 | delete timerIds[id]; 244 | } 245 | 246 | function eventLoop(action) { 247 | while (action) { 248 | try { 249 | action(); 250 | } catch (e) { 251 | if (typeof onerror == "function") { 252 | onerror(e, null, -1); 253 | } else { 254 | throw e; 255 | } 256 | } 257 | action = nextEvent(); 258 | } 259 | } 260 | 261 | // Global properties. "self" refers to the global object, so adding a 262 | // property to "self" defines a global variable. 263 | self.self = self 264 | self.dartMainRunner = function(main, args) { 265 | // Initialize. 266 | var action = function() { main(args); } 267 | eventLoop(action); 268 | }; 269 | self.setTimeout = addTimer; 270 | self.clearTimeout = cancelTimer; 271 | self.setInterval = addInterval; 272 | self.clearInterval = cancelTimer; 273 | self.scheduleImmediate = addTask; 274 | })(self); 275 | """; 276 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apps Script Tools 2 | 3 | This is not an official Google product. It is not supported by the Dart team. 4 | 5 | This package provides tools for using dart2js-compiled programs as Google Apps 6 | scripts. 7 | 8 | The `gsify` program adds boilerplate and necessary preambles, and the 9 | `upload` program uploads the resulting `gs` script to Google Drive. 10 | 11 | The `main` program makes the development process easier by automatically 12 | using those two tools whenever the input JS file is changed. 13 | 14 | See also [clasp](https://github.com/google/clasp) for a similar tool that isn't 15 | specialized for Dart, but supports more operations. 16 | 17 | ## Usage 18 | 19 | The most common use case is to watch the output file of a dart2js 20 | compilation and upload it as Google Apps script whenever it changes. 21 | 22 | This is accomplished with the `main`-script (aka `apps_script_watch` when 23 | enabled through `pub global activate`). 24 | 25 | In its simplest form it just needs two arguments: the input file and 26 | a Google Drive destination. Every time the input file changes (and also 27 | initially at startup) it converts the JS file into a valid 28 | Google Apps script (prefixing it with a necessary preamble) and then 29 | uploads it to Google drive at the given location. (This requires an 30 | OAuth authentication). 31 | 32 | Similar to `gsify` it can also add stub functions (see "Stub Functions" 33 | below) or the 34 | `/* @OnlyCurrentDoc */` or `/* @NotOnlyCurrentDoc */` comments (see 35 | https://developers.google.com/apps-script/guides/services/authorization). 36 | 37 | *Note that Google Apps script must be compiled with the `--cps` flag of 38 | dart2js.* 39 | 40 | Example 41 | ``` 42 | pub global activate apps_script_tools 43 | apps_script_watch in.js folder/script_name 44 | ``` 45 | 46 | ### Gsify 47 | 48 | The `gsify` executable converts a dart2js-compiled program into a valid 49 | Google Apps script. 50 | It prefixes the necessary preamble and optionally add some stub functions, 51 | and `/* @OnlyCurrentDoc */` or `/* @NotOnlyCurrentDoc */` comments (see 52 | https://developers.google.com/apps-script/guides/services/authorization). 53 | 54 | The input file must be the output of `dart2js` with the `--cps` flag. 55 | 56 | Example: 57 | ``` 58 | pub global activate apps_script_tools 59 | apps_script_gsify in.js out.gs 60 | ``` 61 | 62 | The following example adds the `/* @OnlyCurrentDoc */` comment and a 63 | stub-function called `onOpen`: 64 | ``` 65 | apps_script_gsify -s onOpen --only-current-document in.js out.gs 66 | ``` 67 | 68 | ### Upload 69 | 70 | `upload` takes a valid Google Apps script and uploads it to Google Drive. 71 | 72 | If there exists already a Google Apps script at the provided destination 73 | replaces the content with the given input script. This only works if the 74 | existing Google Apps script only contains one source file. 75 | 76 | The destination may be prefixed with folders (which must exist). 77 | 78 | This script uses Google APIs and thus requires an OAuth authentication 79 | which is cached for future uses. 80 | 81 | Example: 82 | ``` 83 | pub global activate apps_script_tools 84 | apps_script_upload in.gs folder/script_name 85 | ``` 86 | 87 | ### Run 88 | 89 | `run` executes the uploaded script. Scripts must be run in the same 90 | Google Cloud project as the Google API that makes the invocation. This 91 | means that the request to run the script must use a clientId/Secret that 92 | is provided by the user. 93 | 94 | See below ("Remote Script Execution") for detailed instructions on how to 95 | set this up. 96 | 97 | 98 | ## Stub Functions 99 | Whenever the Google Apps Script service needs to call into the provided script 100 | it needs to statically see the target function. That is, the provided 101 | JavaScript must contain a function with the given name. For example, 102 | Spreadsheet Addons that want to add a menu entry must have a statically visible 103 | `onOpen` function. The output of dart2js avoids modifying the global environment 104 | and the current JS interop functionality does not give any means to export a 105 | Dart function. To work around this limitation, one can use a stub function that 106 | is then overwritten from within the Dart program. 107 | 108 | Concretely, running `main` or `gsify` with `-s onOpen` will add the following 109 | JavaScript function to the generated `.gs`: 110 | 111 | ``` JavaScript 112 | function onOpen() {} 113 | ``` 114 | 115 | From within Dart one can then use JS interop to overwrite this function before 116 | it is invoked: 117 | 118 | ``` dart 119 | @JS() 120 | library main; 121 | 122 | import 'package:js/js.dart'; 123 | 124 | @JS() 125 | external set onOpen(value); 126 | 127 | void onOpenDart(e) { 128 | // Run on-open things like adding a menu. 129 | } 130 | 131 | main() { 132 | onOpen = allowInterop(onOpenDart); 133 | } 134 | ``` 135 | 136 | This (or a similar setup) must be done for any function that the Apps framework 137 | wants to use as an entry point. This includes simple triggers (see 138 | https://developers.google.com/apps-script/guides/triggers/), the menu entries, 139 | and callbacks from html services 140 | (https://developers.google.com/apps-script/guides/html/reference/run). 141 | 142 | ## Running Scripts 143 | 144 | ### Using the Script Editor 145 | 146 | The easiest way to run a script is to upload it (using one of the provided 147 | tools), and then run a function from within the Script Editor. 148 | 149 | The Script Editor is not connected by default in Google Drive. If the 150 | uploaded script doesn't open on double-click: go to 151 | "New" -> "More" -> "Connect more apps" and connect "Google Apps Script". 152 | 153 | The Editor allows to run (statically visible) functions directly. 154 | 155 | #### Bound Scripts / Addons 156 | Scripts that should be run on an opened document/spreadsheet are called 157 | "bound scripts" or "addons". 158 | 159 | The Script Editor has a convenient 'Test as add-on...' functionality which 160 | opens a file with the script running as add-on. 161 | 162 | ### As a Shared Library 163 | Instead of using Script Editor's "Test as add-on...", one can also create a 164 | bound script directly and then use the uploaded script as a shared library. This 165 | approach has the advantage that the script will also be loaded when the document 166 | is opened outside the editor. It also makes it possible for other users to use 167 | the script without needing to publish it. 168 | 169 | 1. Create a saved version of the script you want to use as a shared library. (File -> Manage versions). 170 | 2. Create a bound script ("Tools" -> "Script Editor" from within a open 171 | file (document, spreadsheet, ...). 172 | 3. Save the project and give it a name. 173 | 4. Link the uploaded script as shared library: Resources -> Libraries -> Add a library. 174 | Don't forget to enable the "Development mode". This way uploads to the script are 175 | immediately visible to you (but not other users). 176 | 177 | Once that's in place, one just needs to forward functions to the shared library. 178 | For example, the following bound script forwards the `onOpen` and `hello` 179 | functions from the bound script to the shared library (imported with the 180 | identifier "dart"): 181 | 182 | ``` JavaScript 183 | function onOpen(e) { dart.onOpen(e); } 184 | function demo() { dart.demo(); } 185 | ``` 186 | 187 | Interestingly, it's not necessary to forward menu functions. In fact, it's 188 | possible to create menu entries that call immediately into the shared 189 | library. In this case, the function does not even need a stub. 190 | 191 | For development it's thus convenient to forward the prefix to Dart's 192 | `onOpen` function: 193 | 194 | ``` JavaScript 195 | function onOpen(e) { dart.onOpen(e, "dart"); } 196 | ``` 197 | 198 | Inside Dart, one can then use this information to set up menu entries 199 | without needing to deal with forwarders or stub functions. 200 | 201 | ``` dart 202 | @JS() 203 | external set onOpen(value); 204 | 205 | @JS() 206 | external set hello(value); 207 | 208 | void onOpenDart(e, [String prefix]) { 209 | if (prefix == null) { 210 | prefix = ""; 211 | } else { 212 | prefix = "$prefix."; 213 | } 214 | SpreadsheetApp 215 | .getUi() 216 | .createMenu("Dart") 217 | .addItem("hello", "${prefix}hello") 218 | .addToUi(); 219 | } 220 | 221 | void helloDart() { 222 | SpreadsheetApp.getUi().alert("Hello World"); 223 | } 224 | 225 | main() { 226 | onOpen = allowInterop(onOpenDart); 227 | hello = allowInterop(helloDart); 228 | } 229 | ``` 230 | 231 | ### Using Google APIs 232 | Google Apps script that have been uploaded to Google Drive can be invoked with 233 | Google API calls. The [Executing Functions using the Apps Script API Guide](https://developers.google.com/apps-script/api/how-tos/execute) 234 | explains how to remotely execute a specified Apps Script function. 235 | 236 | Fundamentally, the call needs the following information: 237 | - the ID of the script project which can be found in the project's 238 | properties (see "Shared Library" above). 239 | - the name of the function to execute. The corresponding Dart function 240 | must be exported with a stub (see above). 241 | - the arguments. 242 | 243 | However, the `scripts.run` API has one important restriction: the script 244 | being called and the calling application must share a Cloud Platform 245 | project. 246 | 247 | One can either use the default one created for each script, or move the 248 | script to a different project: 249 | https://developers.google.com/apps-script/guides/cloud-platform-projects#switch_to_a_different_google_cloud_platform_project 250 | 251 | The walk-through below details the necessary steps. 252 | 253 | 254 | ## Walk-throughs 255 | This section step-by-step instructions on common tasks. 256 | 257 | For all examples we assume a `pubspec.yaml` depending on the 258 | `google-apps` and `js` package: 259 | 260 | ``` 261 | name: example 262 | description: Example for Google Apps scripting in Dart. 263 | version: 0.0.1 264 | #homepage: https://www.example.com 265 | author: 266 | 267 | environment: 268 | sdk: '>=1.20.1 <2.0.0' 269 | 270 | dependencies: 271 | js: ^0.6.1 272 | google_apps: ^0.0.1 273 | ``` 274 | 275 | ### Create Document 276 | In this example we write a script that creates a new document. 277 | 278 | Create the following `bin/doc.dart` file inside your project: 279 | 280 | ``` dart 281 | @JS() 282 | library example; 283 | 284 | import 'package:js/js.dart'; 285 | import 'package:google_apps/document.dart'; 286 | 287 | @JS() 288 | external set create(value); 289 | 290 | void createDart() { 291 | var document = DocumentApp.create("doc- ${new DateTime.now()}"); 292 | var body = document.getBody(); 293 | body.appendParagraph("Created from Dart").editAsText().setFontSize(32); 294 | } 295 | 296 | void main() { 297 | create = allowInterop(createDart); 298 | } 299 | ``` 300 | 301 | Compile it with dart2js: 302 | 303 | ``` 304 | $ dart2js --csp -o out.js bin/doc.dart 305 | ``` 306 | 307 | Upload it Google Drive as a Google Apps script: 308 | ``` 309 | $ apps_script_watch -s create out.js docs_create 310 | ``` 311 | 312 | After authentication, the tool uploads the script as Google Apps script. 313 | 314 | Open it in Google Drive. If it doesn't work ("No preview available") 315 | connect the "Google Apps Script" app first: 316 | 1. New -> More -> Connect more apps 317 | 2. Search for Google Apps Script and connect it. 318 | 319 | In the editor select "create" in the listed functions and press run: 320 | 321 | ![Image of selecting create](pics/select_create.png) 322 | 323 | The first time the script is run it will ask for permissions, and then 324 | create a new Google Docs document. 325 | 326 | ### Hello World Addon 327 | In this section we create a script that is bound to a document. 328 | 329 | First create a new spreadsheet. Alternatively you can create a Google Docs 330 | document, but you would need to change a few lines below. 331 | 332 | Create the following `bin/sheet.dart` program: 333 | 334 | ``` dart 335 | @JS() 336 | library hello_docs; 337 | 338 | import 'package:js/js.dart'; 339 | import 'package:google_apps/spreadsheet.dart'; 340 | 341 | @JS() 342 | external set sayHello(value); 343 | 344 | @JS() 345 | external set onOpen(value); 346 | 347 | void sayHelloDart() { 348 | SpreadsheetApp.getUi().alert("Hello world"); 349 | } 350 | 351 | void onOpenDart(e) { 352 | SpreadsheetApp 353 | .getUi() 354 | .createMenu("from dart") 355 | .addItem("say hello", "sayHello") 356 | .addToUi(); 357 | } 358 | 359 | main(List arguments) { 360 | onOpen = allowInterop(onOpenDart); 361 | sayHello = allowInterop(sayHelloDart); 362 | } 363 | ``` 364 | 365 | Compile it with dart2js: 366 | ``` 367 | dart2js --csp -o out.js bin/sheet.dart 368 | ``` 369 | 370 | Upload it to Google Drive: 371 | ``` 372 | $ apps_script_watch -s onOpen -s sayHello out.js hello 373 | ``` 374 | 375 | Open the uploaded script in the Apps Script Editor. If a double-click 376 | on the script doesn't work ("No preview available") see the first example. 377 | 378 | In the editor run the application by testing it as an addon: 379 | 380 | ![Image of testing as addon](pics/test_as_addon.png) 381 | 382 | Note that the script will add an entry in the `Add-ons` menu. If 383 | the script had been run as a bound script, it would add the menu directly 384 | into the main menu. We can achieve this effect by using the uploaded 385 | code as a shared library. 386 | 387 | #### Shared Library 388 | Go back to the script editor from where we launched the add-on test. 389 | 390 | Save a new version: File -> Manage versions 391 | 392 | Find the script-id. It's available in the URL, or in "File" -> "Project properties". 393 | 394 | Now go back to the spreadsheet where we want to run this script in. 395 | 396 | From within the spreadsheet open the Script editor ("Tools" -> "Script Editor"). 397 | 398 | Copy the following code into the editor (usually "Code.gs"): 399 | 400 | ``` JavaScript 401 | function onOpen(e) { hello.onOpen(e); } 402 | function sayHello() { hello.sayHello(); } 403 | ``` 404 | 405 | Save the project and give it a name. 406 | 407 | Go into "Resources" -> "Libraries". 408 | 409 | Add a new library, using the script-id we retrieved earlier. 410 | 411 | Use the latest version. 412 | 413 | Verify that the "Identifier" is correctly set to "hello". (You can also 414 | use a different identifier, but then you need to update "Code.gs"). 415 | 416 | Enable "Development mode". This way a new upload of the apps script is 417 | immediately used. Note, however, that other users only see the selected 418 | saved version. 419 | 420 | Open the spreadsheet again (or reload it). It should now add a new menu 421 | entry "from dart". 422 | 423 | 424 | ### Remote Script Execution 425 | Start by finishing the "Create Document" tutorial above. You should now 426 | have a script "docs_create" in Google Drive. 427 | 428 | Open it with the Script Editor. 429 | 430 | First retrieve the project id and the necessary scopes for this project: 431 | - "File" -> "Project properties" 432 | - Write down the Script ID 433 | - Switch to the "Scopes" tab 434 | - Write down the listed scopes (it should just be one: 435 | "https://www.googleapis.com/auth/documents"). 436 | 437 | Publish the script so that the Script's API can find it: 438 | "Publish" -> "Deploy as API executable". 439 | 440 | Now go to "Resources" -> "Cloud Platform project". 441 | 442 | In the opened window, the editor shows the project the script is currently 443 | associated with. Click on the link. This brings you to the 444 | "Google Cloud Platform" page. 445 | 446 | In the "Getting Started" section click on 447 | "Enable APIs and get credentials like keys". 448 | 449 | Enable the Scripting API: 450 | - Click on "ENABLE APIS AND SERVICES" 451 | - Search for "Google Apps Script API" and Enable it. 452 | 453 | Create new OAuth credentials: 454 | - Go to the Credentials tab. 455 | - Click on the "Create Credentials" pop-down and select "OAuth client ID". 456 | - Chose "Other" and enter a name. 457 | 458 | A new window reveals a "client ID" and "client secret". 459 | 460 | Use those two strings as follows to run the script remotely from the 461 | command line: 462 | ``` 463 | $ pub global run apps_script_tools:run -s [SCOPE] --client-id [CLIENT_ID] --client-secret [CLIENT_SECRET] [SCRIPT_ID] create 464 | ``` 465 | If there are multiple scopes, each of them must have its own "-s" 466 | parameter to the `run` script. 467 | 468 | If the script takes parameters, they can be added after the function name. 469 | 470 | ## Features and bugs 471 | 472 | Please file feature requests and bugs at the [issue tracker][tracker]. 473 | 474 | [tracker]: https://github.com/google/apps_script_tools/issues 475 | --------------------------------------------------------------------------------