├── core
├── images
│ ├── entry.png
│ ├── delete_file.png
│ ├── delete_folder.png
│ ├── rename_file.png
│ └── rename_folder.png
├── Drive.gs
├── appscript.json
├── fluid-config.yaml
└── Common.gs
├── fluid-Dockerfile
├── .github
└── workflows
│ └── dev.yml
├── LICENSE
└── README.md
/core/images/entry.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycui1984/DriveWorks/HEAD/core/images/entry.png
--------------------------------------------------------------------------------
/core/images/delete_file.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycui1984/DriveWorks/HEAD/core/images/delete_file.png
--------------------------------------------------------------------------------
/core/images/delete_folder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycui1984/DriveWorks/HEAD/core/images/delete_folder.png
--------------------------------------------------------------------------------
/core/images/rename_file.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycui1984/DriveWorks/HEAD/core/images/rename_file.png
--------------------------------------------------------------------------------
/core/images/rename_folder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ycui1984/DriveWorks/HEAD/core/images/rename_folder.png
--------------------------------------------------------------------------------
/fluid-Dockerfile:
--------------------------------------------------------------------------------
1 | # syntax=docker/dockerfile:1
2 | FROM nixos/nix:latest
3 | WORKDIR /usr/scan
4 | COPY . /usr/scan/
5 | RUN mkdir results
6 | RUN nix-env -if https://github.com/fluidattacks/makes/archive/23.04.tar.gz
7 |
--------------------------------------------------------------------------------
/core/Drive.gs:
--------------------------------------------------------------------------------
1 | /**
2 | * Callback for rendering the card for specific Drive items.
3 | * @param {Object} e The event object.
4 | * @return {CardService.Card} The card to show to the user.
5 | */
6 |
7 | function onDriveItemsSelected(e) {
8 | console.log(e);
9 | return createHomeCard(e.drive.activeCursorItem);
10 | }
11 |
12 |
--------------------------------------------------------------------------------
/.github/workflows/dev.yml:
--------------------------------------------------------------------------------
1 | # .github/workflows/dev.yml
2 | name: Makes CI
3 | on: [push, pull_request]
4 | jobs:
5 | machineStandalone:
6 | runs-on: ubuntu-latest
7 | steps:
8 | - uses: actions/checkout@f095bcc56b7c2baf48f3ac70d6d6782f4f553222
9 | - uses: docker://ghcr.io/fluidattacks/makes/amd64:23.06
10 | name: machineStandalone
11 | with:
12 | args: m gitlab:fluidattacks/universe@trunk /skims scan ./core/fluid-config.yaml
13 |
--------------------------------------------------------------------------------
/core/appscript.json:
--------------------------------------------------------------------------------
1 | {
2 | "timeZone": "America/Los_Angeles",
3 | "dependencies": {},
4 | "exceptionLogging": "STACKDRIVER",
5 | "oauthScopes": [
6 | "https://www.googleapis.com/auth/drive.addons.metadata.readonly",
7 | "https://www.googleapis.com/auth/script.locale",
8 | "https://www.googleapis.com/auth/userinfo.email",
9 | "https://www.googleapis.com/auth/drive",
10 | "https://www.googleapis.com/auth/script.scriptapp",
11 | "https://www.googleapis.com/auth/spreadsheets",
12 | "https://www.googleapis.com/auth/script.send_mail"
13 | ],
14 | "runtimeVersion": "V8",
15 | "addOns": {
16 | "common": {
17 | "name": "DriveWorks",
18 | "logoUrl": "https://drive.google.com/uc?id=1YRuaxhrXiRlLu1SbTZopBKTDElfO3QIx",
19 | "useLocaleFromApp": true,
20 | "homepageTrigger": {
21 | "runFunction": "onHomepage",
22 | "enabled": true
23 | },
24 | "universalActions": []
25 | },
26 | "drive": {
27 | "onItemsSelectedTrigger": {
28 | "runFunction": "onDriveItemsSelected"
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 ycui1984
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/core/fluid-config.yaml:
--------------------------------------------------------------------------------
1 | # Description:
2 | # Pick a name you like, normally the name of the repository.
3 | # Example:
4 | namespace: OWASP
5 |
6 | # Description:
7 | # Omit if you want pretty-printed results,
8 | # Set to a path if you want CSV results.
9 | # Optional:
10 | # Yes
11 | # Example:
12 | #output:
13 | # file_path: ./Fluid-Attacks-Results.csv
14 | # format: CSV
15 |
16 | # Description:
17 | # Working directory, normally used as the path to the repository.
18 | # Example:
19 | working_dir: .
20 |
21 | # Description:
22 | # SAST for source code.
23 | # Example:
24 | sast:
25 | # Description:
26 | # Target files used in the analysis.
27 | # Example:
28 | include:
29 | # Absolute path
30 | - .
31 | # Relative path to `working_dir`
32 | - .
33 |
34 | # Description:
35 | # Reversing checks for Android APKs.
36 | # apk:
37 | # Description:
38 | # Target files used in the analysis.
39 | # Example:
40 | # include:
41 | # Absolute paths
42 | - /app/app-arm-debug-Android5.apk
43 | - /app/app-arm-debug.apk
44 | - /app/app-x86-debug-Android5.apk
45 | - /app/app-x86-debug.apk
46 |
47 |
48 | # Description:
49 | # Findings to analyze.
50 | # The complete list of findings can be found here:
51 | # https://gitlab.com/fluidattacks/universe/-/blob/trunk/skims/manifests/findings.lst
52 | # Optional:
53 | # Yes, if not present all security findings will be analyzed.
54 | # Example:
55 | checks:
56 | - F001
57 | - F004
58 | - F008
59 | - F009
60 | - F010
61 | - F011
62 | - F012
63 | - F015
64 | - F016
65 | - F017
66 | - F020
67 | - F021
68 | - F022
69 | - F023
70 | - F031
71 | - F034
72 | - F035
73 | - F037
74 | - F042
75 | - F043
76 | - F052
77 | - F055
78 | - F056
79 | - F058
80 | - F073
81 | - F075
82 | - F079
83 | - F080
84 | - F082
85 | - F085
86 | - F086
87 | - F089
88 | - F091
89 | - F092
90 | - F094
91 | - F096
92 | - F098
93 | - F099
94 | - F100
95 | - F103
96 | - F107
97 | - F112
98 | - F120
99 | - F127
100 | - F128
101 | - F129
102 | - F130
103 | - F131
104 | - F132
105 | - F133
106 | - F134
107 | - F143
108 | - F160
109 | - F176
110 | - F177
111 | - F182
112 | - F200
113 | - F203
114 | - F206
115 | - F207
116 | - F211
117 | - F234
118 | - F239
119 | - F246
120 | - F247
121 | - F250
122 | - F252
123 | - F256
124 | - F257
125 | - F258
126 | - F259
127 | - F266
128 | - F267
129 | - F268
130 | - F277
131 | - F281
132 | - F300
133 | - F313
134 | - F320
135 | - F325
136 | - F333
137 | - F335
138 | - F338
139 | - F346
140 | - F363
141 | - F372
142 | - F380
143 | - F381
144 | - F393
145 | - F394
146 | - F396
147 | - F398
148 | - F400
149 | - F401
150 | - F402
151 | - F406
152 | - F407
153 | - F408
154 | - F409
155 | - F411
156 | - F412
157 | - F413
158 | - F414
159 | - F416
160 | - F418
161 |
162 | # Description:
163 | # Language to use, valid values are: EN, ES.
164 | # Optional:
165 | # Yes, defaults to: EN.
166 | language: EN
167 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DriveWorks
2 |
3 | Managing Google drive files could be time consuming and error prone.
4 | Imagine how boring it was to delete all documents titled as 'Untitled document'
5 | among thousands of files under hundreds of folders. DriveWorks is a workspace
6 | addon to make file or folder management easy. The addon supports operations like
7 | deletion, renaming, etc on files/folders with filters like name matching, file
8 | type matching etc.
9 |
10 | # Design Choice
11 | High level, the implementation of the addon used an async pattern, where user commits the job
12 | spec into property storage first, then later, in the logic of triggers, the user job is executed
13 | and status is reported. Note that executing user jobs immediately after user committed
14 | the job spec (inline execution) is only acceptable for small jobs and is going to be
15 | problematic (Google has limitation of each executation to be under 5 minutes) for jobs
16 | that lasts > 5 minutes or hours.
17 |
18 | To overcome the 5 minutes per executation limitation, the addon set up multiple triggers with
19 | 5 minutes interval. At the end of each executation, we use property service to store the snapshot
20 | of the executation. At the start of each executation, we use property service to resume the last
21 | executation state. Note that the trigger might not fire exactly at the configured time due to
22 | Google trigger limitations. However, setting up multiple triggers mitigate this problem to some degrees
23 | and is acceptable in practice.
24 |
25 | # How To Use This Addon
26 | ## Step1. Clone the repo locally
27 | `git clone https://github.com/ycui1984/DriveWorks.git`
28 |
29 | ## Step2. Create new projects for app scripts
30 | Open `https://script.google.com/home/start`
31 | Click `New Project`
32 |
33 | ## Step3. Move the local files into the created project
34 |
35 | ## Step4. After you have the project
36 | Click `Deploy` and `Test Deployments`
37 |
38 | ## Step5. Goto your google drive. The driveworks addon logo should show up at very right.
39 |
40 | # ScreenShot
41 | ## Main Menu
42 | 
43 |
44 | ## Delete File
45 | 
46 |
47 | ## Delete Folder
48 | 
49 |
50 | ## Rename File
51 | 
52 |
53 | ## Rename Folder
54 | 
55 |
56 | # Demo Video
57 | See https://www.youtube.com/watch?v=MWWHMgO7vP8 as an example on how to manage Google files/folders in batch.
58 |
59 | # Donations
60 | If this addon make your life easier, please consider to donate to encourage development.
61 | Buymeacoffee https://www.buymeacoffee.com/smartgas
62 | Paypal https://www.paypal.com/paypalme/SmartGAS888
63 |
--------------------------------------------------------------------------------
/core/Common.gs:
--------------------------------------------------------------------------------
1 | function onHomepage(e) {
2 | console.log('onHomePage: ' + JSON.stringify(e));
3 | var statusCard = buildCardViaPropertiesIfExist();
4 | if (statusCard) {
5 | console.log('onHomePage: return existing status card');
6 | return statusCard.build();
7 | }
8 | console.log('onHomePage: return brand new home card');
9 | return createHomeCard();
10 | }
11 |
12 | function applyDeleteOnFile(file, dryrun, delete_ops, include_subfolder, sheet) {
13 | var matchStr = getMatchStr(delete_ops);
14 | var fileName = file.getName();
15 | if (matchStr === null || fileName.includes(matchStr)) {
16 | console.log('Deleting file ' + fileName);
17 | if (dryrun) {
18 | sheet.appendRow(["File", "Delete", "Yes", include_subfolder, fileName, "N/A"]);
19 | return;
20 | }
21 |
22 | sheet.appendRow(["File", "Delete", "No", include_subfolder, fileName, "N/A"]);
23 | file.setTrashed(true);
24 | }
25 | }
26 |
27 | function applyDeleteOnFolder(folder, dryrun, delete_ops, include_subfolder, sheet) {
28 | var matchStr = getMatchStr(delete_ops);
29 | var notEmpty = folder.getFiles().hasNext();
30 | var folderName = folder.getName();
31 | if (matchStr === null || folderName.includes(matchStr)) {
32 | if (delete_ops.delete_empty_folder) {
33 | if (notEmpty) return;
34 | }
35 | console.log('Deleting folder ' + folderName);
36 | if (dryrun) {
37 | sheet.appendRow(["Folder", "Delete", "Yes", include_subfolder, folderName, "N/A"]);
38 | return;
39 | }
40 |
41 | sheet.appendRow(["Folder", "Delete", "No", include_subfolder, folderName, "N/A"]);
42 | folder.setTrashed(true);
43 | }
44 | }
45 |
46 |
47 | function getNewName(name, rename_ops) {
48 | if (rename_ops.method === "rename_partial") {
49 | return name.replaceAll(rename_ops.search, rename_ops.replace);
50 | }
51 |
52 | if (rename_ops.method === "rename_full") {
53 | return rename_ops.fullname;
54 | }
55 |
56 | if (rename_ops.method === "rename_adding") {
57 | return ((rename_ops.before === null)? "" : rename_ops.before) + name + ((rename_ops.after === null)? "" : rename_ops.after);
58 | }
59 |
60 | throw "Unsupported rename ops = " + rename_ops.rename_method;
61 | }
62 |
63 | function applyRenameOnFile(file, dryrun, rename_ops, include_subfolder, sheet) {
64 | var matchStr = getMatchStr(rename_ops);
65 | var fileName = file.getName();
66 | if (matchStr === null || fileName.includes(matchStr)) {
67 | var new_name = getNewName(fileName, rename_ops);
68 | console.log('Renaming file ' + fileName + ' into new name = ' + new_name);
69 | if (dryrun) {
70 | sheet.appendRow(["File", "Rename", "Yes", include_subfolder, fileName, new_name]);
71 | return;
72 | }
73 |
74 | sheet.appendRow(["File", "Rename", "No", include_subfolder, fileName, new_name]);
75 | file.setName(new_name);
76 | }
77 | }
78 |
79 | function applyRenameOnFolder(folder, dryrun, rename_ops, include_subfolder, sheet) {
80 | var matchStr = getMatchStr(rename_ops);
81 | var folderName = folder.getName();
82 | if (matchStr === null || folderName.includes(matchStr)) {
83 | var new_name = getNewName(folderName, rename_ops);
84 | console.log('Renaming folder ' + folderName + ' into new name = ' + new_name);
85 | if (dryrun) {
86 | sheet.appendRow(["Folder", "Rename", "Yes", include_subfolder, folderName, new_name]);
87 | return;
88 | }
89 |
90 | sheet.appendRow(["Folder", "Rename", "No", include_subfolder, folderName, new_name]);
91 | folder.setName(new_name);
92 | }
93 | }
94 |
95 | function nextIteration(iterationState, operation, entity, include_subfolder, dryrun, delete_ops, rename_ops, sheet) {
96 | var currentIteration = iterationState[iterationState.length-1];
97 | if (currentIteration.fileIteratorContinuationToken !== null) {
98 | var fileIterator = DriveApp.continueFileIterator(currentIteration.fileIteratorContinuationToken);
99 | if (fileIterator.hasNext()) {
100 | var file = fileIterator.next();
101 | if (entity === "file") {
102 | if (operation === "delete") {
103 | applyDeleteOnFile(file, dryrun, delete_ops, include_subfolder, sheet);
104 | } else if (operation === "rename") {
105 | applyRenameOnFile(file, dryrun, rename_ops, include_subfolder, sheet);
106 | } else {
107 | throw "nextIteration: Unsupported operation type for file = " + operation;
108 | }
109 | }
110 | currentIteration.fileIteratorContinuationToken = fileIterator.getContinuationToken();
111 | iterationState[iterationState.length-1] = currentIteration;
112 | return iterationState;
113 | }
114 |
115 | currentIteration.fileIteratorContinuationToken = null;
116 | iterationState[iterationState.length-1] = currentIteration;
117 | return iterationState;
118 | }
119 |
120 | if (currentIteration.folderIteratorContinuationToken !== null) {
121 | var folderIterator = DriveApp.continueFolderIterator(currentIteration.folderIteratorContinuationToken);
122 | if (folderIterator.hasNext()) {
123 | var folder = folderIterator.next();
124 | if (entity === "folder") {
125 | if (operation === "delete") {
126 | applyDeleteOnFolder(folder, dryrun, delete_ops, include_subfolder, sheet);
127 | } else if (operation === "rename") {
128 | applyRenameOnFolder(folder, dryrun, rename_ops, include_subfolder, sheet);
129 | } else {
130 | throw "nextIteration: Unsupported operation type for folder = " + operation;
131 | }
132 | }
133 | iterationState[iterationState.length-1].folderIteratorContinuationToken = folderIterator.getContinuationToken();
134 | if (include_subfolder) {
135 | iterationState.push(makeIterationFromFolder(folder, operation, entity, delete_ops, rename_ops));
136 | }
137 | return iterationState;
138 | }
139 |
140 | iterationState.pop();
141 | return iterationState;
142 | }
143 | }
144 |
145 | function getMatchStr(ops) {
146 | return ops.search;
147 | }
148 |
149 | function getMimeType(ops) {
150 | var type = ops.file_type;
151 | if (type === "spreadsheet") return MimeType.GOOGLE_SHEETS;
152 | if (type === "doc") return MimeType.GOOGLE_DOCS;
153 | if (type === "slide") return MimeType.GOOGLE_SLIDES;
154 | if (type === "form") return MimeType.GOOGLE_FORMS;
155 | if (type === "sites") return MimeType.GOOGLE_SITES;
156 | if (type === "drawing") return MimeType.GOOGLE_DRAWINGS;
157 | if (type === "appscript") return MimeType.GOOGLE_APPS_SCRIPT;
158 | if (type === "pdf") return MimeType.PDF;
159 | if (type === "bmp") return MimeType.BMP;
160 | if (type === "gif") return MimeType.GIF;
161 | if (type === "jpeg") return MimeType.JPEG;
162 | if (type === "png") return MimeType.PNG;
163 | if (type === "svg") return MimeType.SVG;
164 | if (type === "css") return MimeType.CSS;
165 | if (type === "csv") return MimeType.CSV;
166 | if (type === "html") return MimeType.HTML;
167 | if (type === "js") return MimeType.JAVASCRIPT;
168 | if (type === "txt") return MimeType.PLAIN_TEXT;
169 | if (type === "rtf") return MimeType.RTF;
170 | if (type === "zip") return MimeType.ZIP;
171 | if (type === "word1") return MimeType.MICROSOFT_WORD_LEGACY;
172 | if (type === "word2") return MimeType.MICROSOFT_WORD;
173 | if (type === "excel1") return MimeType.MICROSOFT_EXCEL_LEGACY;
174 | if (type === "excel2") return MimeType.MICROSOFT_EXCEL;
175 | if (type === "ppt1") return MimeType.MICROSOFT_POWERPOINT_LEGACY;
176 | if (type === "ppt2") return MimeType.MICROSOFT_POWERPOINT;
177 | if (type === "odt") return MimeType.OPENDOCUMENT_TEXT;
178 | if (type === "ods") return MimeType.OPENDOCUMENT_SPREADSHEET;
179 | if (type === "odp") return MimeType.OPENDOCUMENT_PRESENTATION;
180 | if (type === "odg") return MimeType.OPENDOCUMENT_GRAPHICS;
181 | return null;
182 | }
183 |
184 | function getSearchToken(folder, entity, mimeType) {
185 | var searchStr = mimeType === null? null : "mimeType='" + mimeType + "'";
186 | if (searchStr === null) {
187 | return null;
188 | }
189 | console.log('Get token via search string = ' + searchStr);
190 | return (entity === "file")? folder.searchFiles(searchStr).getContinuationToken() : folder.searchFolders(searchStr).getContinuationToken();
191 | }
192 |
193 | function makeIterationFromFolder(folder, operation, entity, delete_ops, rename_ops) {
194 | var iteration = {
195 | folderName: folder.getName(),
196 | fileIteratorContinuationToken: null,
197 | folderIteratorContinuationToken: null
198 | };
199 |
200 | if (operation === "delete") {
201 | if (entity === "file") {
202 | console.log('mime type = ' + getMimeType(delete_ops) + ', match str = ' + getMatchStr(delete_ops));
203 | var token = getSearchToken(folder, entity, getMimeType(delete_ops));
204 | if (token !== null) {
205 | console.log('search file token is not null = ' + token);
206 | iteration.fileIteratorContinuationToken = token;
207 | }
208 | } else {
209 | var token = getSearchToken(folder, entity, null);
210 | if (token !== null) {
211 | iteration.folderIteratorContinuationToken = token;
212 | }
213 | }
214 | } else if (operation === "rename") {
215 | if (entity === "file") {
216 | var token = getSearchToken(folder, entity, getMimeType(rename_ops));
217 | if (token !== null) {
218 | iteration.fileIteratorContinuationToken = token;
219 | }
220 | } else {
221 | var token = getSearchToken(folder, entity, null);
222 | if (token !== null) {
223 | iteration.folderIteratorContinuationToken = token;
224 | }
225 | }
226 | }
227 |
228 | if (iteration.fileIteratorContinuationToken === null) {
229 | iteration.fileIteratorContinuationToken = folder.getFiles().getContinuationToken();
230 | }
231 |
232 | if (iteration.folderIteratorContinuationToken === null) {
233 | iteration.folderIteratorContinuationToken = folder.getFolders().getContinuationToken();
234 | }
235 |
236 | /// now iteration has both file token and folder token
237 | /// when entity is folder only do not set file token because it is useless
238 | if (entity === "folder") {
239 | iteration.fileIteratorContinuationToken = null;
240 | }
241 |
242 | return iteration;
243 | }
244 |
245 | function getJobStatusKey(email) {
246 | return email + ":job";
247 | }
248 |
249 | function getJobMetadataKey(email) {
250 | return email + ":metadata";
251 | }
252 |
253 | function deleteAllTriggersForUser() {
254 | // NOTE: getProjectTrigger() only list out triggers for CURRENT USER
255 | var triggers = ScriptApp.getProjectTriggers();
256 | for (var i = 0; i < triggers.length; i++) {
257 | ScriptApp.deleteTrigger(triggers[i]);
258 | }
259 | }
260 |
261 | function getLockKey(email) {
262 | return email + "lock";
263 | }
264 |
265 | function onScheduledRun(e) {
266 | var email = Session.getEffectiveUser().getEmail();
267 | var jobMetadataKey = getJobMetadataKey(email);
268 | var properties = PropertiesService.getUserProperties();
269 | var metadata = properties.getProperty(jobMetadataKey);
270 | if (metadata === null) {
271 | console.log('onScheduleRun: Cannot find job metadata for key ' + jobMetadataKey + '. Skip since no work to do');
272 | return;
273 | }
274 |
275 | var lockVal = properties.getProperty(getLockKey(email));
276 | if (lockVal !== null) {
277 | console.log('There is another callback to make progress. Skip');
278 | return;
279 | }
280 |
281 | var timestamp = (new Date()).getTime().toString();
282 | properties.setProperty(getLockKey(email), timestamp);
283 | var json = JSON.parse(metadata);
284 | var folder = DriveApp.getFolderById(json.folder.id);
285 | var spreadsheet = SpreadsheetApp.openById(json.report_spreadsheet.id);
286 | var sheet = spreadsheet.getSheetByName(getOperationLogSheetName());
287 | var progress = spreadsheet.getSheetByName(getProgressSheetName());
288 | try {
289 | iterateFolder(folder, json.operation, json.entity, json.include_subfolder, json.dry_run, json.delete_ops, json.rename_ops, sheet, progress, timestamp);
290 | } catch(e) {
291 | console.log('error in onScheduledRun:' + e);
292 | }
293 | properties.deleteProperty(getLockKey(email));
294 | }
295 |
296 | function iterateFolder(folder, operation, entity, include_subfolder, dryrun, delete_ops, rename_ops, sheet, progress, timestamp) {
297 | var email = Session.getEffectiveUser().getEmail();
298 | console.log('Iterate entity in folder: email = ' + email + ', folder = ' + folder + ', operation = ' + operation + ', entity = ' + entity + ', include_subfolder = ' + include_subfolder + ', dryrun = ' + dryrun + ', delete_ops = ' + JSON.stringify(delete_ops) + ', rename_ops = ' + JSON.stringify(rename_ops) + ', lock was hold at = ' + timestamp);
299 | var MAX_RUNNING_TIME_MS = 4.5 * 60 * 1000;
300 | var startTime = (new Date()).getTime();
301 | var jobKey = getJobStatusKey(email);
302 | var jobMetadataKey = getJobMetadataKey(email);
303 | var lockKey = getLockKey(email);
304 | var properties = PropertiesService.getUserProperties();
305 | var jobValue = properties.getProperty(jobKey);
306 | var iterationState = JSON.parse(jobValue);
307 | if (iterationState !== null) {
308 | if (folder.getName() !== iterationState[0].folderName) {
309 | console.error("Iterating a new folder: " + folder.getName() + ". End early since existing operation is not done.");
310 | return;
311 | }
312 | console.info("Resuming iteration for folder: " + folder.getName());
313 | }
314 | if (iterationState === null) {
315 | console.info("Starting new iteration for folder: " + folder.getName());
316 | progress.appendRow([new Date(), "STARTED"]);
317 | iterationState = [];
318 | iterationState.push(makeIterationFromFolder(folder, operation, entity, delete_ops, rename_ops));
319 | }
320 |
321 | progress.appendRow([new Date(), "STARTED NEW ITERATION"]);
322 | while (iterationState.length > 0) {
323 | iterationState = nextIteration(iterationState, operation, entity, include_subfolder, dryrun, delete_ops, rename_ops, sheet);
324 | var currTime = (new Date()).getTime();
325 | var elapsedTimeInMS = currTime - startTime;
326 | var timeLimitExceeded = elapsedTimeInMS >= MAX_RUNNING_TIME_MS;
327 | if (timeLimitExceeded) {
328 | if (properties.getProperty(lockKey) !== timestamp || properties.getProperty(jobKey) !== jobValue) {
329 | console.info('Lock has been overwritten or jobValue has been overwritten, which means others committed at race condition time. Abort without committing.');
330 | progress.appendRow([new Date(), "ENDED NEW ITERATION WITH OUT COMMITTING"]);
331 | } else {
332 | properties.setProperty(jobKey, JSON.stringify(iterationState));
333 | progress.appendRow([new Date(), "ENDED NEW ITERATION"]);
334 | }
335 | console.info("Stopping loop after '%d' milliseconds.", elapsedTimeInMS);
336 | return;
337 | }
338 | }
339 |
340 | console.info("Done iterating. Deleting iterating state ... ");
341 | progress.appendRow([new Date(), "ENDED NEW ITERATION"]);
342 | progress.appendRow([new Date(), "DONE"]);
343 | var url = sheet.getParent().getUrl();
344 | sendCompletionEmail(email, url);
345 | properties.deleteProperty(jobKey);
346 | properties.deleteProperty(jobMetadataKey);
347 | deleteAllTriggersForUser();
348 | }
349 |
350 | function sendCompletionEmail(email, url) {
351 | var message = 'Dear DriveWorks User,\n\n The job has been completed, please check the progress spreadsheet for the details. Here is the spreadsheet URL ' + url + '.\n\n\nThanks!';
352 | console.log('sendUsageExpireEmail:' + email + ', message = ' + message);
353 | MailApp.sendEmail(email, 'Job completed by DriveWorks', message);
354 | }
355 |
356 |
357 | function parseFolderFromEvent(e) {
358 | var folderId = null;
359 | if (('selectedItems' in e.drive) && (e.drive.activeCursorItem.mimeType === 'application/vnd.google-apps.folder')) {
360 | folderId = e.drive.activeCursorItem.id;
361 | }
362 | console.log('folderId = ' + folderId);
363 | if (folderId === null) {
364 | var folder = DriveApp.getRootFolder();
365 | } else {
366 | var folder = DriveApp.getFolderById(folderId);
367 | }
368 | return folder;
369 | }
370 |
371 | function getOperationLogSheetName() {
372 | return "Operation Logs";
373 | }
374 |
375 | function getProgressSheetName() {
376 | return "Progress";
377 | }
378 |
379 | function setMetadata(folder, operation, entity, dryrun, include_subfolder, delete_ops, rename_ops) {
380 | var email = Session.getEffectiveUser().getEmail();
381 | var properties = PropertiesService.getUserProperties();
382 | var jobMetadataKey = getJobMetadataKey(email);
383 | var spreadsheet = SpreadsheetApp.create("DriveWorks_progress_report_" + Date.now());
384 | var sheet = spreadsheet.insertSheet(getOperationLogSheetName());
385 | sheet.appendRow(["Entity", "Operation", "Dry run", "Include subfolder", "Entity name", "Updated name"]);
386 | var progress = spreadsheet.insertSheet(getProgressSheetName());
387 | progress.appendRow(["Time", "Progress"]);
388 | spreadsheet.deleteSheet(spreadsheet.getSheetByName('Sheet1'));
389 | var metadata = {
390 | folder: {
391 | id: folder.getId(),
392 | name: folder.getName()
393 | },
394 | operation : operation,
395 | entity : entity,
396 | include_subfolder : include_subfolder,
397 | dry_run : dryrun,
398 | delete_ops : delete_ops,
399 | rename_ops : rename_ops,
400 | report_spreadsheet : {
401 | id : spreadsheet.getId(),
402 | name: spreadsheet.getName()
403 | }
404 | };
405 | if (properties.getProperty(jobMetadataKey) === null) {
406 | properties.setProperty(jobMetadataKey, JSON.stringify(metadata));
407 | var jobKey = getJobStatusKey(email);
408 | properties.deleteProperty(jobKey);
409 | } else {
410 | console.log('job metadata key exists, but it should not, the key =' + jobMetadataKey);
411 | }
412 | }
413 |
414 | function onScheduledRunNear0(e) {
415 | var email = Session.getEffectiveUser().getEmail();
416 | console.log('onScheduledRunNear0 for email = ' + email);
417 | return onScheduledRun(e);
418 | }
419 |
420 | function onScheduledRunNear5(e) {
421 | var email = Session.getEffectiveUser().getEmail();
422 | console.log('onScheduledRunNear5 for email = ' + email);
423 | return onScheduledRun(e);
424 | }
425 |
426 | function onScheduledRunNear10(e) {
427 | var email = Session.getEffectiveUser().getEmail();
428 | console.log('onScheduledRunNear10 for email = ' + email);
429 | return onScheduledRun(e);
430 | }
431 |
432 | function onScheduledRunNear15(e) {
433 | var email = Session.getEffectiveUser().getEmail();
434 | console.log('onScheduledRunNear15 for email = ' + email);
435 | return onScheduledRun(e);
436 | }
437 |
438 | function onScheduledRunNear20(e) {
439 | var email = Session.getEffectiveUser().getEmail();
440 | console.log('onScheduledRunNear20 for email = ' + email);
441 | return onScheduledRun(e);
442 | }
443 |
444 | function onScheduledRunNear25(e) {
445 | var email = Session.getEffectiveUser().getEmail();
446 | console.log('onScheduledRunNear25 for email = ' + email);
447 | return onScheduledRun(e);
448 | }
449 |
450 | function onScheduledRunNear30(e) {
451 | var email = Session.getEffectiveUser().getEmail();
452 | console.log('onScheduledRunNear30 for email = ' + email);
453 | return onScheduledRun(e);
454 | }
455 |
456 | function onScheduledRunNear35(e) {
457 | var email = Session.getEffectiveUser().getEmail();
458 | console.log('onScheduledRunNear35 for email = ' + email);
459 | return onScheduledRun(e);
460 | }
461 |
462 | function onScheduledRunNear40(e) {
463 | var email = Session.getEffectiveUser().getEmail();
464 | console.log('onScheduledRunNear40 for email = ' + email);
465 | return onScheduledRun(e);
466 | }
467 |
468 | function onScheduledRunNear45(e) {
469 | var email = Session.getEffectiveUser().getEmail();
470 | console.log('onScheduledRunNear45 for email = ' + email);
471 | return onScheduledRun(e);
472 | }
473 |
474 | function onScheduledRunNear50(e) {
475 | var email = Session.getEffectiveUser().getEmail();
476 | console.log('onScheduledRunNear50 for email = ' + email);
477 | return onScheduledRun(e);
478 | }
479 |
480 | function onScheduledRunNear55(e) {
481 | var email = Session.getEffectiveUser().getEmail();
482 | console.log('onScheduledRunNear55 for email = ' + email);
483 | return onScheduledRun(e);
484 | }
485 |
486 | function installTriggersForUsers() {
487 | var email = Session.getEffectiveUser().getEmail();
488 | deleteAllTriggersForUser();
489 | // for (var i = 0; i < 60; i+= 5) {
490 | // console.log('installTriggersForUsers: email = ' + email + ', near min = ' + i);
491 | // ScriptApp.newTrigger("onScheduledRunNear" + i.toString()).timeBased().everyHours(1).nearMinute(i).create();
492 | // }
493 | console.log('installTriggersForUsers: email = ' + email + ', near min = 0');
494 | ScriptApp.newTrigger("onScheduledRunNear0").timeBased().everyHours(1).nearMinute(0).create();
495 | }
496 |
497 | function deleteFileHandler(e) {
498 | console.log('deleteFileHandler = ' + JSON.stringify(e));
499 | var folder = parseFolderFromEvent(e);
500 | var filename_match = ('file_name_field' in e.formInput)? e.formInput.file_name_field : null;
501 | var delete_ops = {
502 | file_type: e.formInput.file_type_field,
503 | search: filename_match,
504 | delete_empty_folder: null,
505 | }
506 | var dryrun = JSON.parse(e.parameters.dryrun);
507 | var include_subfolder = JSON.parse(e.parameters.include_subfolder);
508 | setMetadata(folder, "delete", "file", dryrun, include_subfolder, delete_ops, null);
509 | installTriggersForUsers();
510 | var card = createDeleteFileCard(include_subfolder, dryrun);
511 | var navigation = CardService.newNavigation().updateCard(card);
512 | var actionResponse = CardService.newActionResponseBuilder()
513 | .setNavigation(navigation);
514 | return actionResponse.build();
515 | }
516 |
517 | function deleteJob() {
518 | var email = Session.getEffectiveUser().getEmail();
519 | var jobMetadataKey = getJobMetadataKey(email);
520 | var properties = PropertiesService.getUserProperties();
521 | properties.deleteProperty(jobMetadataKey);
522 | return createHomeCard();
523 | }
524 |
525 |
526 | function buildCardViaPropertiesIfExist() {
527 | var properties = PropertiesService.getUserProperties();
528 | var email = Session.getEffectiveUser().getEmail();
529 | var jobMetadataKey = getJobMetadataKey(email);
530 | var metadataStr = properties.getProperty(jobMetadataKey);
531 | if (metadataStr === null) {
532 | return null;
533 | }
534 | var metadata = JSON.parse(metadataStr);
535 | var cardHeader = CardService.newCardHeader()
536 | .setTitle("DriveWorks")
537 | .setSubtitle("Job Settings and Status")
538 | .setImageUrl(getLogoURL());
539 | var paymentSection = getPaymentSection();
540 | var mainSection = CardService.newCardSection();
541 | var operationType = CardService.newDecoratedText().setText("Drive operation type: " + metadata.operation + "").setWrapText(true).setTopLabel("Primary Job Settings");
542 | var entityType = CardService.newDecoratedText().setText("Entity type: " + metadata.entity + "").setWrapText(true);
543 | var folderName = CardService.newDecoratedText().setText("Target folder: " + metadata.folder.name + "");
544 | var dryrun = CardService.newDecoratedText().setText("Is dryrun: " + metadata.dry_run + "");
545 | var include_subfolder = CardService.newDecoratedText().setText("Include subfolder: " + metadata.include_subfolder + "");
546 | mainSection
547 | .addWidget(operationType)
548 | .addWidget(entityType)
549 | .addWidget(folderName)
550 | .addWidget(dryrun)
551 | .addWidget(include_subfolder);
552 | var spreadsheet = SpreadsheetApp.openById(metadata.report_spreadsheet.id);
553 | var link = CardService.newOpenLink()
554 | .setUrl(spreadsheet.getUrl())
555 | .setOpenAs(CardService.OpenAs.FULL_SIZE)
556 | .setOnClose(CardService.OnClose.RELOAD_ADD_ON)
557 | var status = CardService.newDecoratedText().setText("CLICK HERE to monitor job progress. You will be notified via email once the job completes. You are not allowed to start a new job while a previous job is running unless the previous is deleted.").setWrapText(true).setTopLabel("Existing Job Status").setOpenLink(link);
558 | mainSection.addWidget(status);
559 | var action = CardService.newAction()
560 | .setFunctionName('deleteJob')
561 | var button = CardService.newTextButton()
562 | .setText('Delete Job')
563 | .setOnClickAction(action)
564 | .setTextButtonStyle(CardService.TextButtonStyle.FILLED);
565 | var buttonSet = CardService.newButtonSet()
566 | .addButton(button);
567 | mainSection.addWidget(buttonSet);
568 | var card = CardService.newCardBuilder()
569 | .setHeader(cardHeader)
570 | .addSection(paymentSection)
571 | .addSection(mainSection);
572 |
573 | return card;
574 | }
575 |
576 | function deleteFolderHandler(e) {
577 | console.log('deleteFolderHandler = ' + JSON.stringify(e));
578 | var folder = parseFolderFromEvent(e);
579 | var foldername_match = ('folder_name_field' in e.formInput)? e.formInput.folder_name_field : null;
580 | var delete_empty_folder = ('delete_empty_folders_field' in e.formInput)? true : false;
581 | var delete_ops = {
582 | file_type: null,
583 | search: foldername_match,
584 | delete_empty_folder: delete_empty_folder,
585 | }
586 | var dryrun = JSON.parse(e.parameters.dryrun);
587 | var include_subfolder = JSON.parse(e.parameters.include_subfolder);
588 | setMetadata(folder, "delete", "folder", dryrun, include_subfolder, delete_ops, null);
589 | installTriggersForUsers();
590 | var card = createDeleteFolderCard(include_subfolder, dryrun);
591 | var navigation = CardService.newNavigation().updateCard(card);
592 | var actionResponse = CardService.newActionResponseBuilder()
593 | .setNavigation(navigation);
594 | return actionResponse.build();
595 | }
596 |
597 | function renameFileHandler(e) {
598 | console.log('renameFileHandler = ' + JSON.stringify(e));
599 | var folder = parseFolderFromEvent(e);
600 | var rename_ops = {
601 | method: e.formInput.rename_method_field,
602 | file_type: e.formInput.file_type_field,
603 | search: ("file_name_search_field" in e.formInput)? e.formInput.file_name_search_field : null,
604 | replace: ("file_name_replace_field" in e.formInput)? e.formInput.file_name_replace_field : null,
605 | fullname: ("new_file_name_field" in e.formInput)? e.formInput.new_file_name_field : null,
606 | before: ("file_name_before_field" in e.formInput)? e.formInput.file_name_before_field: null,
607 | after: ("file_name_after_field" in e.formInput)? e.formInput.file_name_after_field : null,
608 | }
609 |
610 | var dryrun = JSON.parse(e.parameters.dryrun);
611 | var include_subfolder = JSON.parse(e.parameters.include_subfolder);
612 | setMetadata(folder, "rename", "file", dryrun, include_subfolder, null, rename_ops);
613 | installTriggersForUsers();
614 | var card = createRenameFileCard(e.formInput.rename_method_field, include_subfolder, dryrun);
615 | var navigation = CardService.newNavigation().updateCard(card);
616 | var actionResponse = CardService.newActionResponseBuilder()
617 | .setNavigation(navigation);
618 | return actionResponse.build();
619 | }
620 |
621 | function renameFolderHandler(e) {
622 | console.log('renameFolderHandler = ' + JSON.stringify(e));
623 | var folder = parseFolderFromEvent(e);
624 | var rename_ops = {
625 | method: e.formInput.rename_method_field,
626 | file_type: null,
627 | search: ("folder_name_search_field" in e.formInput)? e.formInput.folder_name_search_field : null,
628 | replace: ("folder_name_replace_field" in e.formInput)? e.formInput.folder_name_replace_field : null,
629 | fullname: ("new_folder_name_field" in e.formInput)? e.formInput.new_folder_name_field : null,
630 | before: ("folder_name_before_field" in e.formInput)? e.formInput.folder_name_before_field: null,
631 | after: ("folder_name_after_field" in e.formInput)? e.formInput.folder_name_after_field : null,
632 | }
633 | var dryrun = JSON.parse(e.parameters.dryrun);
634 | var include_subfolder = JSON.parse(e.parameters.include_subfolder);
635 | setMetadata(folder, "rename", "folder", dryrun, include_subfolder, null, rename_ops);
636 | installTriggersForUsers();
637 | var card = createRenameFolderCard(e.formInput.rename_method_field, include_subfolder, dryrun);
638 | var navigation = CardService.newNavigation().updateCard(card);
639 | var actionResponse = CardService.newActionResponseBuilder()
640 | .setNavigation(navigation);
641 | return actionResponse.build();
642 | }
643 |
644 | function getFileTypeWidget() {
645 | return CardService.newSelectionInput()
646 | .setType(CardService.SelectionInputType.DROPDOWN)
647 | .setTitle("Delete when file type matches")
648 | .setFieldName("file_type_field")
649 | .addItem("Any file type", "all", true)
650 | .addItem("Google Sheets", "spreadsheet", false)
651 | .addItem("Google Docs", "doc", false)
652 | .addItem("Google Slides", "slide", false)
653 | .addItem("Google Forms", "form", false)
654 | .addItem("Google Sites", "sites", false)
655 | .addItem("Google Drawings", "drawing", false)
656 | .addItem("Google App Script", "appscript", false)
657 | .addItem("Adobe PDF (.pdf)", "pdf", false)
658 | .addItem("BMP file (.bmp)", "bmp", false)
659 | .addItem("GIF file (.gif)", "gif", false)
660 | .addItem("JPEG file (.jpeg)", "jpeg", false)
661 | .addItem("PNG file (.png)", "png", false)
662 | .addItem("SVG file (.svg)", "svg", false)
663 | .addItem("CSS file (.css)", "css", false)
664 | .addItem("CSV file (.csv)", "csv", false)
665 | .addItem("Html file (.html)", "html", false)
666 | .addItem("Javascript file (.js)", "js", false)
667 | .addItem("Plain Text (.txt)", "txt", false)
668 | .addItem("Rich Text file (.rtf)", "rtf", false)
669 | .addItem("ZIP file (.zip)", "zip", false)
670 | .addItem("Microsoft Word (.doc)", "word1", false)
671 | .addItem("Microsoft Word (.docx)", "word2", false)
672 | .addItem("Microsoft Excel (.xls)", "excel1", false)
673 | .addItem("Microsoft Excel (.xlsx)", "excel2", false)
674 | .addItem("Microsoft Powerpoint (.ppt)", "ppt1", false)
675 | .addItem("Microsoft Powerpoint (.pptx)", "ppt2", false)
676 | .addItem("OpenDocument Text (.odt)", "odt", false)
677 | .addItem("OpenDocument Spreadsheet (.ods)", "ods", false)
678 | .addItem("OpenDocument Presentation (.odp)", "odp", false)
679 | .addItem("OpenDocument Graphics (.odg)", "odg", false);
680 | }
681 |
682 | function createDeleteFileCard(include_subfolder, dryrun) {
683 | var statusCard = buildCardViaPropertiesIfExist();
684 | if (statusCard) {
685 | return statusCard.build();
686 | }
687 | var cardHeader = CardService.newCardHeader()
688 | .setTitle("DriveWorks")
689 | .setSubtitle("Delete files")
690 | .setImageUrl(getLogoURL());
691 | var paymentSection = getPaymentSection();
692 | var mainSection = CardService.newCardSection().setHeader("Set filters for files to delete");
693 |
694 | var filenameMatch = CardService.newTextInput()
695 | .setFieldName("file_name_field")
696 | .setTitle("Delete when file name contains input");
697 | mainSection.addWidget(filenameMatch);
698 |
699 | var fileType = getFileTypeWidget();
700 | mainSection.addWidget(fileType);
701 |
702 | var action = CardService.newAction()
703 | .setFunctionName('deleteFileHandler')
704 | .setParameters({include_subfolder: JSON.stringify(include_subfolder), dryrun : JSON.stringify(dryrun)});
705 | var button = CardService.newTextButton()
706 | .setText('Delete Files')
707 | .setOnClickAction(action)
708 | .setTextButtonStyle(CardService.TextButtonStyle.FILLED);
709 | var buttonSet = CardService.newButtonSet()
710 | .addButton(button);
711 | mainSection.addWidget(buttonSet);
712 | var status = CardService.newDecoratedText().setText("File deletion progress is going to be shown in a spreadsheet once button is clicked. You will be notified via email once the job completes.").setWrapText(true);
713 | mainSection.addWidget(status);
714 |
715 | var card = CardService.newCardBuilder()
716 | .setHeader(cardHeader)
717 | .addSection(paymentSection)
718 | .addSection(mainSection);
719 |
720 | return card.build();
721 | }
722 |
723 | function createDeleteFolderCard(include_subfolder, dryrun) {
724 | var statusCard = buildCardViaPropertiesIfExist();
725 | if (statusCard) {
726 | return statusCard.build();
727 | }
728 | var cardHeader = CardService.newCardHeader()
729 | .setTitle("DriveWorks")
730 | .setSubtitle("Delete folders")
731 | .setImageUrl(getLogoURL());
732 | var paymentSection = getPaymentSection();
733 | var mainSection = CardService.newCardSection().setHeader("Set filters for folders to delete");
734 |
735 | var deleteEmpty = CardService.newSelectionInput()
736 | .setType(CardService.SelectionInputType.CHECK_BOX)
737 | .setFieldName("delete_empty_folders_field")
738 | .addItem("Delete empty folders only", "delete_empty_folder", false);
739 | mainSection.addWidget(deleteEmpty);
740 |
741 | var foldernameMatch = CardService.newTextInput()
742 | .setFieldName("folder_name_field")
743 | .setTitle("Delete when folder name contains input");
744 | mainSection.addWidget(foldernameMatch);
745 |
746 | var action = CardService.newAction()
747 | .setFunctionName('deleteFolderHandler')
748 | .setParameters({include_subfolder: JSON.stringify(include_subfolder), dryrun : JSON.stringify(dryrun)});
749 |
750 | var button = CardService.newTextButton()
751 | .setText('Delete Folders')
752 | .setOnClickAction(action)
753 | .setTextButtonStyle(CardService.TextButtonStyle.FILLED);
754 | var buttonSet = CardService.newButtonSet()
755 | .addButton(button);
756 | mainSection.addWidget(buttonSet);
757 |
758 | var status = CardService.newDecoratedText().setText("Folder deletion progress is going to be shown in a spreadsheet once button is clicked. You will be notified via email once the job completes.").setWrapText(true).setTopLabel("Folder deletion status is shown below");
759 | mainSection.addWidget(status);
760 |
761 | var card = CardService.newCardBuilder()
762 | .setHeader(cardHeader)
763 | .addSection(paymentSection)
764 | .addSection(mainSection);
765 |
766 | return card.build();
767 | }
768 |
769 | function changeFileRenameHandler(e) {
770 | console.log('changeFileRenameHandler = ' + JSON.stringify(e));
771 | var card = createRenameFileCard(e.formInput.rename_method_field, JSON.parse(e.parameters.include_subfolder), JSON.parse(e.parameters.dryrun));
772 | var navigation = CardService.newNavigation().updateCard(card);
773 | var actionResponse = CardService.newActionResponseBuilder()
774 | .setNavigation(navigation);
775 | return actionResponse.build();
776 | }
777 |
778 | function createRenameFileCard(rename_method="rename_partial", include_subfolder, dryrun) {
779 | var statusCard = buildCardViaPropertiesIfExist();
780 | if (statusCard) {
781 | return statusCard.build();
782 | }
783 | var cardHeader = CardService.newCardHeader()
784 | .setTitle("DriveWorks")
785 | .setSubtitle("Rename file")
786 | .setImageUrl(getLogoURL());
787 | var paymentSection = getPaymentSection();
788 | var mainSection = CardService.newCardSection().setHeader("Set filters to rename files");
789 | var action = CardService.newAction()
790 | .setFunctionName('changeFileRenameHandler')
791 | .setParameters({include_subfolder: JSON.stringify(include_subfolder), dryrun : JSON.stringify(dryrun)});
792 | var renameMethod = CardService.newSelectionInput()
793 | .setType(CardService.SelectionInputType.RADIO_BUTTON)
794 | .setFieldName("rename_method_field")
795 | .setOnChangeAction(action);
796 | if (rename_method === "rename_partial") {
797 | renameMethod
798 | .addItem("Replace matched file name", "rename_partial", true)
799 | .addItem("Rename full name", "rename_full", false)
800 | .addItem("Add string before or after file name", "rename_adding", false);
801 | } else if (rename_method === "rename_full") {
802 | renameMethod
803 | .addItem("Replace matched file name", "rename_partial", false)
804 | .addItem("Rename full name", "rename_full", true)
805 | .addItem("Add string before or after file name", "rename_adding", false);
806 | } else if (rename_method === "rename_adding") {
807 | renameMethod
808 | .addItem("Replace matched file name", "rename_partial", false)
809 | .addItem("Rename full name", "rename_full", false)
810 | .addItem("Add string before or after file name", "rename_adding", true);
811 | } else {
812 | throw "Unsupported rename method = " + rename_method;
813 | }
814 |
815 | mainSection.addWidget(renameMethod);
816 | if (rename_method === "rename_partial") {
817 | var search = CardService.newTextInput()
818 | .setFieldName("file_name_search_field")
819 | .setTitle("String to match file name");
820 | var replace = CardService.newTextInput()
821 | .setFieldName("file_name_replace_field")
822 | .setTitle("String to replace the matches");
823 | mainSection.addWidget(search).addWidget(replace);
824 | } else if (rename_method === "rename_full") {
825 | var newname = CardService.newTextInput()
826 | .setFieldName("new_file_name_field")
827 | .setTitle("New file name");
828 | mainSection.addWidget(newname);
829 | } else if (rename_method === "rename_adding") {
830 | var before = CardService.newTextInput()
831 | .setFieldName("file_name_before_field")
832 | .setTitle("Add string before file name");
833 | var after = CardService.newTextInput()
834 | .setFieldName("file_name_after_field")
835 | .setTitle("Add string after file name");
836 | mainSection.addWidget(before).addWidget(after);
837 | } else {
838 | throw "Unsupported rename method X = " + rename_method;
839 | }
840 |
841 | var fileType = getFileTypeWidget();
842 | mainSection.addWidget(fileType);
843 |
844 | var action = CardService.newAction()
845 | .setFunctionName('renameFileHandler')
846 | .setParameters({include_subfolder: JSON.stringify(include_subfolder), dryrun : JSON.stringify(dryrun)});
847 | var button = CardService.newTextButton()
848 | .setText('Rename Files')
849 | .setOnClickAction(action)
850 | .setTextButtonStyle(CardService.TextButtonStyle.FILLED);
851 | var buttonSet = CardService.newButtonSet()
852 | .addButton(button);
853 | mainSection.addWidget(buttonSet);
854 | var status = CardService.newDecoratedText().setText("File renaming progress is going to be shown in a spreadsheet once button is clicked. You will be notified via email once the job completes.").setWrapText(true).setTopLabel("File renaming status is shown below");
855 | mainSection.addWidget(status);
856 | var card = CardService.newCardBuilder()
857 | .setHeader(cardHeader)
858 | .addSection(paymentSection)
859 | .addSection(mainSection);
860 |
861 | return card.build();
862 | }
863 |
864 | function changeFolderRenameHandler(e) {
865 | console.log('changeFolderRenameHandler = ' + JSON.stringify(e));
866 | var card = createRenameFolderCard(e.formInput.rename_method_field, JSON.parse(e.parameters.include_subfolder), JSON.parse(e.parameters.dryrun));
867 | var navigation = CardService.newNavigation().updateCard(card);
868 | var actionResponse = CardService.newActionResponseBuilder()
869 | .setNavigation(navigation);
870 | return actionResponse.build();
871 | }
872 |
873 | function createRenameFolderCard(rename_method="rename_partial", include_subfolder, dryrun) {
874 | var statusCard = buildCardViaPropertiesIfExist();
875 | if (statusCard) {
876 | return statusCard.build();
877 | }
878 | var cardHeader = CardService.newCardHeader()
879 | .setTitle("DriveWorks")
880 | .setSubtitle("Rename folder")
881 | .setImageUrl(getLogoURL());
882 | var paymentSection = getPaymentSection();
883 | var mainSection = CardService.newCardSection().setHeader("Set filters to rename folders");
884 | var action = CardService.newAction()
885 | .setFunctionName('changeFolderRenameHandler')
886 | .setParameters({include_subfolder: JSON.stringify(include_subfolder), dryrun: JSON.stringify(dryrun)});
887 | var renameMethod = CardService.newSelectionInput()
888 | .setType(CardService.SelectionInputType.RADIO_BUTTON)
889 | .setFieldName("rename_method_field")
890 | .setOnChangeAction(action);
891 | if (rename_method === "rename_partial") {
892 | renameMethod
893 | .addItem("Replace matched folder name", "rename_partial", true)
894 | .addItem("Rename full name", "rename_full", false)
895 | .addItem("Add string before or after folder name", "rename_adding", false);
896 | } else if (rename_method === "rename_full") {
897 | renameMethod
898 | .addItem("Replace matched folder name", "rename_partial", false)
899 | .addItem("Rename full name", "rename_full", true)
900 | .addItem("Add string before or after folder name", "rename_adding", false);
901 | } else if (rename_method === "rename_adding") {
902 | renameMethod
903 | .addItem("Replace matched folder name", "rename_partial", false)
904 | .addItem("Rename full name", "rename_full", false)
905 | .addItem("Add string before or after folder name", "rename_adding", true);
906 | } else {
907 | throw "Unsupported rename method = " + rename_method;
908 | }
909 |
910 | mainSection.addWidget(renameMethod);
911 | if (rename_method === "rename_partial") {
912 | var search = CardService.newTextInput()
913 | .setFieldName("folder_name_search_field")
914 | .setTitle("String to match folder name");
915 | var replace = CardService.newTextInput()
916 | .setFieldName("folder_name_replace_field")
917 | .setTitle("String to replace the matches");
918 | mainSection.addWidget(search).addWidget(replace);
919 | } else if (rename_method === "rename_full") {
920 | var newname = CardService.newTextInput()
921 | .setFieldName("new_folder_name_field")
922 | .setTitle("New folder name");
923 | mainSection.addWidget(newname);
924 | } else if (rename_method === "rename_adding") {
925 | var before = CardService.newTextInput()
926 | .setFieldName("folder_name_before_field")
927 | .setTitle("Add string before folder name");
928 | var after = CardService.newTextInput()
929 | .setFieldName("folder_name_after_field")
930 | .setTitle("Add string after folder name");
931 | mainSection.addWidget(before).addWidget(after);
932 | } else {
933 | throw "Unsupported rename method X = " + rename_method;
934 | }
935 |
936 | var action = CardService.newAction()
937 | .setFunctionName('renameFolderHandler')
938 | .setParameters({include_subfolder: JSON.stringify(include_subfolder), dryrun : JSON.stringify(dryrun)});
939 |
940 | var button = CardService.newTextButton()
941 | .setText('Rename Folders')
942 | .setOnClickAction(action)
943 | .setTextButtonStyle(CardService.TextButtonStyle.FILLED);
944 | var buttonSet = CardService.newButtonSet()
945 | .addButton(button);
946 | mainSection.addWidget(buttonSet);
947 | var status = CardService.newDecoratedText().setText("Folder renaming progress is going to be shown in a spreadsheet once button is clicked. You will be notified via email once the job completes.").setWrapText(true).setTopLabel("Folder renaming status is shown below");
948 | mainSection.addWidget(status);
949 | var card = CardService.newCardBuilder()
950 | .setHeader(cardHeader)
951 | .addSection(paymentSection)
952 | .addSection(mainSection);
953 |
954 | return card.build();
955 | }
956 |
957 | function configureMore(e) {
958 | console.log('configureMore = ' + JSON.stringify(e));
959 | var expire_time = getExpireTime();
960 | if (expire_time!== -1 && expire_time <= Date.now()/1000) {
961 | var card = getBuymoreCard();
962 | var navigation = CardService.newNavigation()
963 | .pushCard(card);
964 | var actionResponse = CardService.newActionResponseBuilder()
965 | .setNavigation(navigation);
966 | return actionResponse.build();
967 | }
968 |
969 | var operation_type = e.formInput.drive_operation_type_field;
970 | var entity_type = e.formInput.entity_type_field;
971 | var include_subfolder = e.formInput.include_subfolders_field === "include_subfolder";
972 | var dryrun = e.formInput.dryrun_field === "dryrun";
973 | var card = null;
974 | if (operation_type === "delete") {
975 | var card = (entity_type === "file")? createDeleteFileCard(include_subfolder, dryrun) : createDeleteFolderCard(include_subfolder, dryrun);
976 | }
977 |
978 | if (operation_type === "rename") {
979 | var card = (entity_type === "file")? createRenameFileCard("rename_partial", include_subfolder, dryrun) : createRenameFolderCard("rename_partial", include_subfolder, dryrun);
980 | }
981 |
982 | if (card === null) {
983 | throw "Unsupported operation type = " + operation_type;
984 | }
985 |
986 | var navigation = CardService.newNavigation()
987 | .pushCard(card);
988 | var actionResponse = CardService.newActionResponseBuilder()
989 | .setNavigation(navigation);
990 | return actionResponse.build();
991 | }
992 |
993 | function getExpireTime(version=1) {
994 | try {
995 | var initial_days = 7;
996 | var email = Session.getEffectiveUser().getEmail();
997 | var key = email + version.toString();
998 | var properties = PropertiesService.getScriptProperties();
999 | var expire_time = properties.getProperty(key);
1000 | if (expire_time === null) {
1001 | var now = Date.now()/1000;
1002 | var future = now + initial_days*24*3600;
1003 | expire_time = properties.setProperty(key, future.toString()).getProperty(key);
1004 | }
1005 | console.log('expire_time = ' + expire_time);
1006 | var ret = parseInt(expire_time);
1007 | console.log('getExpireTime:' + key + ', initial_days = ' + initial_days + ', expire_time = ' + expire_time);
1008 | return ret;
1009 | } catch (e) {
1010 | console.log('getExpireTime error:' + e);
1011 | throw e;
1012 | }
1013 | }
1014 |
1015 | function getRemainingDays() {
1016 | try {
1017 | var email = Session.getEffectiveUser().getEmail();
1018 | var expire_time = getExpireTime();
1019 | if (expire_time === -1) {
1020 | return -1;
1021 | }
1022 | var now = Date.now()/1000;
1023 | if (expire_time <= now) {
1024 | return 0;
1025 | }
1026 |
1027 | var days = Math.ceil((expire_time - now) / (24*3600));
1028 | console.log('Email:' + email + ',remaining days = ' + days);
1029 | return days;
1030 | } catch (e) {
1031 | console.log('getRemainingDays error:' + e);
1032 | throw e;
1033 | }
1034 | }
1035 |
1036 | function getBuymoreCard() {
1037 | var email = Session.getEffectiveUser().getEmail();
1038 | var cardHeader = CardService.newCardHeader()
1039 | .setTitle("DriveWorks")
1040 | .setSubtitle("Buy More")
1041 | .setImageUrl(getLogoURL());
1042 | var mainSection = CardService.newCardSection().setHeader("Buy more days for app access");
1043 | var description = CardService.newDecoratedText().setText("To support users and add features, we are kindly asking to pay for its use. We hope you find the subscription price reasonable.").setWrapText(true);
1044 | mainSection.addWidget(description);
1045 |
1046 | var options = CardService.newSelectionInput()
1047 | .setType(CardService.SelectionInputType.RADIO_BUTTON)
1048 | .setTitle("Select payment option")
1049 | .setFieldName("payment_option_field")
1050 | .addItem("$5 per month (billed per month)", "month", false)
1051 | .addItem("$50 per year (billed per year)", "year", true);
1052 | mainSection.addWidget(options);
1053 |
1054 | var paypalLink = CardService.newOpenLink()
1055 | .setUrl("https://www.paypal.com/paypalme/SmartGAS888")
1056 | .setOpenAs(CardService.OpenAs.FULL_SIZE)
1057 | .setOnClose(CardService.OnClose.RELOAD_ADD_ON);
1058 | var paypal = CardService.newDecoratedText().setText("Option1 (PayPal): Please use PayPal and send an email to james.cui.code@gmail.com after the payment to update your access.").setWrapText(true).setOpenLink(paypalLink);
1059 | mainSection.addWidget(paypal);
1060 |
1061 | var buycoffeelink = CardService.newOpenLink()
1062 | .setUrl("https://www.buymeacoffee.com/smartgas")
1063 | .setOpenAs(CardService.OpenAs.FULL_SIZE)
1064 | .setOnClose(CardService.OnClose.RELOAD_ADD_ON)
1065 | var buycoffee = CardService.newDecoratedText().setText("Option2 (Buy Me A Coffee): Please use Buy me a coffee and send an email to james.cui.code@gmail.com after the payment to update your access.").setWrapText(true).setOpenLink(buycoffeelink);
1066 | mainSection.addWidget(buycoffee);
1067 |
1068 | if (email === "james.cui.code@gmail.com") {
1069 | var email_account = CardService.newTextInput()
1070 | .setFieldName("email_field")
1071 | .setTitle("Email account");
1072 | var added_days = CardService.newTextInput()
1073 | .setFieldName("added_days")
1074 | .setTitle("Days");
1075 | mainSection.addWidget(email_account).addWidget(added_days);
1076 | var setDaysAction = CardService.newAction().setMethodName("setDays")
1077 | var button = CardService.newTextButton()
1078 | .setText('Set Days')
1079 | .setOnClickAction(setDaysAction)
1080 | .setTextButtonStyle(CardService.TextButtonStyle.FILLED);
1081 | var buttonSet = CardService.newButtonSet()
1082 | .addButton(button);
1083 | mainSection.addWidget(buttonSet);
1084 | }
1085 |
1086 | var card = CardService.newCardBuilder()
1087 | .setHeader(cardHeader)
1088 | .addSection(mainSection);
1089 |
1090 | return card.build();
1091 | }
1092 |
1093 | function setDays(e) {
1094 | var email = e.formInput.email_field;
1095 | var days = parseInt(e.formInput.added_days);
1096 | setExpiredTime(email, 1, days);
1097 | }
1098 |
1099 | function setExpiredTime(user, version, days) {
1100 | try {
1101 | var key = user + version.toString();
1102 | var properties = PropertiesService.getScriptProperties();
1103 | if (days === -1) {
1104 | properties.setProperty(key, "-1");
1105 | return -1;
1106 | }
1107 | var expire_time = properties.getProperty(key);
1108 | var now = Date.now()/1000;
1109 | var future = now + days*24*3600;
1110 | expire_time = properties.setProperty(key, future.toString()).getProperty(key);
1111 | var ret = parseInt(expire_time);
1112 | console.log('setExpiredTime: key = ' + key + ', expire_time = ' + expire_time);
1113 | return ret;
1114 | } catch (e) {
1115 | console.log('setExpiredTime error:' + e);
1116 | return -1;
1117 | }
1118 | }
1119 |
1120 | function buymore() {
1121 | var card = getBuymoreCard();
1122 | var navigation = CardService.newNavigation()
1123 | .pushCard(card);
1124 | var actionResponse = CardService.newActionResponseBuilder()
1125 | .setNavigation(navigation);
1126 | return actionResponse.build();
1127 | }
1128 |
1129 | function getPaymentSection() {
1130 | var endIcon = CardService.newIconImage().setIcon(CardService.Icon.VIDEO_PLAY);
1131 | var buymoreIcon = CardService.newIconImage().setIcon(CardService.Icon.DOLLAR);
1132 | var remainDays = getRemainingDays();
1133 | var action = CardService.newAction().setFunctionName('buymore');
1134 | var buymore = CardService.newDecoratedText().setText("Remaining " + remainDays + " days for trial").setStartIcon(buymoreIcon).setEndIcon(endIcon).setWrapText(true).setOnClickAction(action);
1135 | return CardService.newCardSection().addWidget(buymore);
1136 | }
1137 |
1138 | function getLogoURL() {
1139 | return "https://drive.google.com/uc?id=1YRuaxhrXiRlLu1SbTZopBKTDElfO3QIx";
1140 | }
1141 |
1142 | function createHomeCard(item={}) {
1143 | var entityType = CardService.newSelectionInput()
1144 | .setType(CardService.SelectionInputType.RADIO_BUTTON)
1145 | .setTitle("Select drive entity type")
1146 | .setFieldName("entity_type_field")
1147 | .addItem("File", "file", true)
1148 | .addItem("Folder", "folder", false);
1149 | var operationType = CardService.newSelectionInput()
1150 | .setType(CardService.SelectionInputType.RADIO_BUTTON)
1151 | .setTitle("Select drive operation type")
1152 | .setFieldName("drive_operation_type_field")
1153 | .addItem("Delete", "delete", true)
1154 | .addItem("Rename", "rename", false);
1155 | var textStart = "Selected operation folder
";
1156 | var textEnd = "";
1157 | if ('title' in item) {
1158 | var text = textStart + item.title + textEnd;
1159 | } else {
1160 | var text = textStart + "My Drive" + textEnd;
1161 | }
1162 | var selectedFolder = CardService.newDecoratedText().setText(text).setWrapText(true).setBottomLabel("Change by selecting a different folder");
1163 |
1164 | var dryrun = CardService.newSelectionInput()
1165 | .setType(CardService.SelectionInputType.CHECK_BOX)
1166 | .setFieldName("dryrun_field")
1167 | .addItem("Preview operations only without executing (dryrun)", "dryrun", false);
1168 |
1169 | var includeSubfolder = CardService.newSelectionInput()
1170 | .setType(CardService.SelectionInputType.CHECK_BOX)
1171 | .setFieldName("include_subfolders_field")
1172 | .addItem("Apply to subfolders", "include_subfolder", true);
1173 |
1174 | var configureMoreAction = CardService.newAction()
1175 | .setFunctionName('configureMore');
1176 | var button = CardService.newTextButton()
1177 | .setText('Configure Details')
1178 | .setOnClickAction(configureMoreAction)
1179 | .setTextButtonStyle(CardService.TextButtonStyle.FILLED);
1180 | var buttonSet = CardService.newButtonSet()
1181 | .addButton(button);
1182 | var mainSection = CardService.newCardSection()
1183 | .addWidget(operationType)
1184 | .addWidget(entityType)
1185 | .addWidget(selectedFolder)
1186 | .addWidget(dryrun)
1187 | .addWidget(includeSubfolder)
1188 | .addWidget(buttonSet);
1189 |
1190 | var paymentSection = getPaymentSection();
1191 | var cardHeader = CardService.newCardHeader()
1192 | .setTitle("DriveWorks")
1193 | .setSubtitle("Drive operations made easy")
1194 | .setImageUrl(getLogoURL());
1195 | var card = CardService.newCardBuilder()
1196 | .setHeader(cardHeader)
1197 | .addSection(paymentSection)
1198 | .addSection(mainSection);
1199 |
1200 | return card.build();
1201 | }
1202 |
1203 |
--------------------------------------------------------------------------------