├── 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 | 
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 | 
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 |
--------------------------------------------------------------------------------