@mcpher/gas-fakes 2.3.11 → 2.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -14
- package/gas-fakes.js +1 -0
- package/gf_agent/scripts/builder.js +26 -1
- package/package.json +1 -1
- package/src/cli/lib-manager.js +14 -4
- package/src/cli/setup.js +17 -4
- package/src/services/chartsapp/fakechartsapp.js +6 -1
- package/src/services/driveapp/driveiterators.js +53 -8
- package/src/services/driveapp/fakedriveapp.js +49 -15
- package/src/services/driveapp/fakedrivefile.js +105 -0
- package/src/services/driveapp/fakedrivefolder.js +8 -0
- package/src/services/driveapp/fakedrivemeta.js +68 -16
- package/src/services/driveapp/fakefolderapp.js +19 -6
- package/src/services/enums/chartsenums.js +53 -20
- package/src/services/enums/driveenums.js +1 -0
- package/src/services/slidesapp/fakeautofit.js +23 -15
- package/src/services/slidesapp/fakecolorscheme.js +160 -0
- package/src/services/slidesapp/fakelayout.js +11 -1
- package/src/services/slidesapp/fakemaster.js +10 -0
- package/src/services/slidesapp/fakepresentation.js +27 -0
- package/src/services/slidesapp/fakeslide.js +9 -0
- package/src/services/slidesapp/faketextrange.js +6 -11
- package/src/services/spreadsheetapp/chartenummapping.js +15 -0
- package/src/services/spreadsheetapp/fakebooleancondition.js +119 -0
- package/src/services/spreadsheetapp/fakecellimage.js +42 -0
- package/src/services/spreadsheetapp/fakecellimagebuilder.js +59 -0
- package/src/services/spreadsheetapp/fakeconditionalformatrule.js +55 -0
- package/src/services/spreadsheetapp/fakeconditionalformatrulebuilder.js +330 -0
- package/src/services/spreadsheetapp/fakedevelopermetadata.js +32 -4
- package/src/services/spreadsheetapp/fakedevelopermetadatalocation.js +27 -5
- package/src/services/spreadsheetapp/fakeembeddedchartbuilder.js +155 -21
- package/src/services/spreadsheetapp/fakegradientcondition.js +71 -0
- package/src/services/spreadsheetapp/fakesheet.js +63 -0
- package/src/services/spreadsheetapp/fakesheetrange.js +12 -1
- package/src/services/spreadsheetapp/fakespreadsheet.js +30 -11
- package/src/services/spreadsheetapp/fakespreadsheetapp.js +21 -3
- package/src/services/urlfetchapp/app.js +33 -1
- package/src/support/fileiterators.js +3 -1
- package/src/support/filesharers.js +7 -3
- package/src/support/peeker.js +8 -2
- package/src/support/sheetutils.js +1 -0
- package/src/support/sxdrive.js +26 -15
- package/src/support/syncit.js +4 -6
- package/src/support/workersync/synchronizer.js +24 -4
- package/src/support/workersync/worker.js +13 -2
- package/summarize_advanced.js +0 -69
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ Now you can run apps script code directly from your console - for example
|
|
|
35
35
|
gas-fakes -s "const files=DriveApp.getRootFolder().searchFiles('title contains \"Untitled\"');while (files.hasNext()) {console.log(files.next().getName())};"
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
For details see [gas fakes cli](gas-fakes-cli.md)
|
|
38
|
+
For details see [gas fakes cli](notes/gas-fakes-cli.md)
|
|
39
39
|
|
|
40
40
|
### Configuration
|
|
41
41
|
|
|
@@ -168,7 +168,7 @@ There are a couple of syntactical differences between Node and Apps Script. Not
|
|
|
168
168
|
// this required on Node but not on Apps Script
|
|
169
169
|
if (ScriptApp.isFake) testFakes()
|
|
170
170
|
````
|
|
171
|
-
For inspiration on pushing modified files to the IDE, see the togas.sh bash script I use for the test suite. There's also a complete push pull workflow available - see - [push test pull](pull-test-push.md)
|
|
171
|
+
For inspiration on pushing modified files to the IDE, see the togas.sh bash script I use for the test suite. There's also a complete push pull workflow available - see - [push test pull](notes/pull-test-push.md)
|
|
172
172
|
|
|
173
173
|
|
|
174
174
|
## Help
|
|
@@ -188,11 +188,11 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
|
|
|
188
188
|
- [readme](README.md)
|
|
189
189
|
- [Natural Language Automation with Gemini Skills & MCP Server](gemini-skills-mcp.md) - new skills-based agent approach.
|
|
190
190
|
- [gf_agent documentation](../gf_agent/README.md) - instructions for the Gemini CLI automation agent and MCP server.
|
|
191
|
-
- [gas fakes cli](gas-fakes-cli.md)
|
|
191
|
+
- [gas fakes cli](notes/gas-fakes-cli.md)
|
|
192
192
|
- [github actions using adc](https://github.com/brucemcpherson/gas-fakes-actions-adc)
|
|
193
193
|
- [github actions using dwd and wif](https://github.com/brucemcpherson/gas-fakes-actions-dwd)
|
|
194
|
-
- [ksuite as a back end](ksuite_poc.md)
|
|
195
|
-
- [msgraph as a back end](msgraph.md)
|
|
194
|
+
- [ksuite as a back end](notes/ksuite_poc.md)
|
|
195
|
+
- [msgraph as a back end](notes/msgraph.md)
|
|
196
196
|
- [resurrecting scriptDb repo](https://github.com/brucemcpherson/scriptdb-redux)
|
|
197
197
|
- [Resurrecting ScriptDb – nosql database for Apps Script](https://ramblings.mcpher.com/resurrecting-scriptdb-nosql-database-for-apps-script/)
|
|
198
198
|
- [gas-fakes in serverless containers](https://docs.google.com/presentation/d/1JlXF9T--DD4ERHopyP3WyAMhjRCxxHblgCP5ynxaJ3k/edit?usp=sharing)
|
|
@@ -203,7 +203,7 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
|
|
|
203
203
|
- [running gas-fakes on Amazon AWS lambda](https://github.com/brucemcpherson/gas-fakes-containers)
|
|
204
204
|
- [running gas-fakes on Azure ACA](https://github.com/brucemcpherson/gas-fakes-containers)
|
|
205
205
|
- [running gas-fakes on Github actions](https://github.com/brucemcpherson/gas-fakes-containers)
|
|
206
|
-
- [jdbc notes](jdbc-notes.md)
|
|
206
|
+
- [jdbc notes](notes/jdbc-notes.md)
|
|
207
207
|
- [Yes – you can run native apps script code on Azure ACA as well!](https://ramblings.mcpher.com/yes-you-can-run-native-apps-script-code-on-azure-aca-as-well/)
|
|
208
208
|
- [Yes – you can run native apps script code on AWS Lambda!](https://ramblings.mcpher.com/apps-script-on-aws-lambda/)
|
|
209
209
|
- [initial idea and thoughts](https://ramblings.mcpher.com/a-proof-of-concept-implementation-of-apps-script-environment-on-node/)
|
|
@@ -213,16 +213,16 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
|
|
|
213
213
|
- [Turning async into synch on Node using workers](https://ramblings.mcpher.com/turning-async-into-synch-on-node-using-workers/)
|
|
214
214
|
- [All about Apps Script Enums and how to fake them](https://ramblings.mcpher.com/all-about-apps-script-enums-and-how-to-fake-them/)
|
|
215
215
|
- [colaborators](collaborators.md) - additional information for collaborators
|
|
216
|
-
- [oddities](oddities.md) - a collection of oddities uncovered during this project
|
|
217
|
-
- [named colors](named-colors.md)
|
|
218
|
-
- [sandbox](sandbox.md)
|
|
219
|
-
- [senstive scopes](workspace_scopes.md)
|
|
220
|
-
- [using apps script libraries with gas-fakes](libraries.md)
|
|
216
|
+
- [oddities](notes/oddities.md) - a collection of oddities uncovered during this project
|
|
217
|
+
- [named colors](notes/named-colors.md)
|
|
218
|
+
- [sandbox](notes/sandbox.md)
|
|
219
|
+
- [senstive scopes](notes/workspace_scopes.md)
|
|
220
|
+
- [using apps script libraries with gas-fakes](notes/libraries.md)
|
|
221
221
|
- [how libhandler works](libhandler.md)
|
|
222
222
|
- [article:using apps script libraries with gas-fakes](https://ramblings.mcpher.com/how-to-use-apps-script-libraries-directly-from-node/)
|
|
223
|
-
- [named range identity](named-range-identity.md)
|
|
224
|
-
- [Workspace scopes with local authentication](workspace_scopes.md)
|
|
225
|
-
- [push test pull](pull-test-push.md)
|
|
223
|
+
- [named range identity](notes/named-range-identity.md)
|
|
224
|
+
- [Workspace scopes with local authentication](notes/workspace_scopes.md)
|
|
225
|
+
- [push test pull](notes/pull-test-push.md)
|
|
226
226
|
- [sharing cache and properties between gas-fakes and live apps script](https://ramblings.mcpher.com/sharing-cache-and-properties-between-gas-fakes-and-live-apps-script/)
|
|
227
227
|
- [gas-fakes-cli now has built in mcp server and gemini extension](https://ramblings.mcpher.com/gas-fakes-cli-now-has-built-in-mcp-server-and-gemini-extension/)
|
|
228
228
|
- [gas-fakes CLI: Run apps script code directly from your terminal](https://ramblings.mcpher.com/gas-fakes-cli-run-apps-script-code-directly-from-your-terminal/)
|
package/gas-fakes.js
CHANGED
|
@@ -56,7 +56,32 @@ async function build() {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
await fs.writeFile(INDEX_FILE, masterIndex);
|
|
59
|
-
|
|
59
|
+
|
|
60
|
+
// Aggregate knowledge files into SKILL.md
|
|
61
|
+
const TEMPLATE_FILE = './gf_agent/scripts/SKILL.template.md';
|
|
62
|
+
const KNOWLEDGE_DIR = './gf_agent/knowledge';
|
|
63
|
+
const SKILL_OUTPUT = './gf_agent/SKILL.md';
|
|
64
|
+
|
|
65
|
+
let skillMarkdown = await fs.readFile(TEMPLATE_FILE, 'utf-8');
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const knowledgeFiles = await fs.readdir(KNOWLEDGE_DIR);
|
|
69
|
+
// Sort files to ensure deterministic aggregation (e.g., 01-drive.md, 02-syntax.md)
|
|
70
|
+
knowledgeFiles.sort();
|
|
71
|
+
|
|
72
|
+
for (const kFile of knowledgeFiles) {
|
|
73
|
+
if (kFile.endsWith('.md')) {
|
|
74
|
+
const kContent = await fs.readFile(path.join(KNOWLEDGE_DIR, kFile), 'utf-8');
|
|
75
|
+
skillMarkdown += `\n${kContent}\n`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.log("No knowledge directory found or error reading it:", err.message);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
await fs.writeFile(SKILL_OUTPUT, skillMarkdown);
|
|
83
|
+
|
|
84
|
+
console.log('Build complete! Skills, Index, and monolithic SKILL.md generated.');
|
|
60
85
|
}
|
|
61
86
|
|
|
62
87
|
build().catch(console.error);
|
package/package.json
CHANGED
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"name": "@mcpher/gas-fakes",
|
|
42
42
|
"author": "bruce mcpherson",
|
|
43
|
-
"version": "2.3.
|
|
43
|
+
"version": "2.3.13",
|
|
44
44
|
"license": "MIT",
|
|
45
45
|
"main": "main.js",
|
|
46
46
|
"description": "An implementation of the Google Workspace Apps Script runtime: Run native App Script Code on Node and Cloud Run",
|
package/src/cli/lib-manager.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import { parse } from "acorn";
|
|
3
|
-
import { Auth } from "../support/auth.js"; // Relative to gas-fakes-cli directory
|
|
3
|
+
import { Auth, _identities } from "../support/auth.js"; // Relative to gas-fakes-cli directory
|
|
4
4
|
import { checkForGcloudCli, spawnCommand } from "./utils.js";
|
|
5
5
|
|
|
6
6
|
async function getAccessToken(pattern) {
|
|
@@ -8,12 +8,22 @@ async function getAccessToken(pattern) {
|
|
|
8
8
|
// Authorization pattern 1
|
|
9
9
|
// We use cloud-platform as it provides sufficient access for Drive API fetching
|
|
10
10
|
// while avoiding the 'well-known client ID' block that targets Workspace scopes like drive.readonly.
|
|
11
|
-
const
|
|
11
|
+
const client = await Auth.setAuth(
|
|
12
12
|
["https://www.googleapis.com/auth/cloud-platform"],
|
|
13
13
|
true
|
|
14
14
|
);
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
if (client.cachedCredential !== undefined) {
|
|
16
|
+
client.cachedCredential = null;
|
|
17
|
+
}
|
|
18
|
+
const tokenResponse = await client.getAccessToken();
|
|
19
|
+
const token = tokenResponse.token || tokenResponse;
|
|
20
|
+
|
|
21
|
+
// CRITICAL: We MUST clear the global identities map so that this temporary
|
|
22
|
+
// library-fetching auth does not trick the downstream executor.js and syncit.js
|
|
23
|
+
// into thinking the full environment (and the background worker) has been initialized.
|
|
24
|
+
_identities.clear();
|
|
25
|
+
|
|
26
|
+
return token;
|
|
17
27
|
} else {
|
|
18
28
|
// Authorization pattern 2
|
|
19
29
|
await checkForGcloudCli();
|
package/src/cli/setup.js
CHANGED
|
@@ -622,9 +622,22 @@ export async function initializeConfiguration(options = {}) {
|
|
|
622
622
|
|
|
623
623
|
if (skillResponse.installSkills) {
|
|
624
624
|
console.log("Installing Gemini CLI skills and MCP server...");
|
|
625
|
+
let manualSkillCmd;
|
|
625
626
|
try {
|
|
626
|
-
// 1. Install the agent skill
|
|
627
|
-
|
|
627
|
+
// 1. Install or link the agent skill
|
|
628
|
+
let skillCmd;
|
|
629
|
+
const localSkillPath = path.resolve(process.cwd(), "gf_agent", "SKILL.md");
|
|
630
|
+
const isLocalClone = fs.existsSync(localSkillPath);
|
|
631
|
+
|
|
632
|
+
if (isLocalClone) {
|
|
633
|
+
console.log("Detected local gas-fakes repository. Linking local skill for development...");
|
|
634
|
+
skillCmd = "gemini skills link ./gf_agent";
|
|
635
|
+
manualSkillCmd = "1. gemini skills link ./gf_agent";
|
|
636
|
+
} else {
|
|
637
|
+
skillCmd = "gemini skills install https://github.com/brucemcpherson/gas-fakes.git --path gf_agent";
|
|
638
|
+
manualSkillCmd = "1. gemini skills install https://github.com/brucemcpherson/gas-fakes.git --path gf_agent";
|
|
639
|
+
}
|
|
640
|
+
|
|
628
641
|
console.log(`Executing: ${skillCmd}`);
|
|
629
642
|
execSync(skillCmd, { stdio: "inherit" });
|
|
630
643
|
|
|
@@ -635,11 +648,11 @@ export async function initializeConfiguration(options = {}) {
|
|
|
635
648
|
|
|
636
649
|
console.log("\x1b[1;32mInstallation complete!\x1b[0m");
|
|
637
650
|
console.log("\nYou can now use natural language to automate tasks:");
|
|
638
|
-
console.log(" \x1b[1;33m
|
|
651
|
+
console.log(" \x1b[1;33m\“Create a sheet called ‘Todays drive files' and add any files on Drive modified today to it\"\x1b[0m");
|
|
639
652
|
} catch (err) {
|
|
640
653
|
console.error(`\x1b[1;31mError during Gemini installation: ${err.message}\x1b[0m`);
|
|
641
654
|
console.log("You may need to install them manually:");
|
|
642
|
-
console.log(
|
|
655
|
+
console.log(manualSkillCmd);
|
|
643
656
|
console.log("2. gemini mcp add --scope project gas-fakes-mcp gas-fakes mcp");
|
|
644
657
|
}
|
|
645
658
|
} else {
|
|
@@ -19,7 +19,12 @@ export const newFakeChartsApp = (...args) => {
|
|
|
19
19
|
export class FakeChartsApp {
|
|
20
20
|
constructor() {
|
|
21
21
|
const enumProps = [
|
|
22
|
-
"ChartType", //
|
|
22
|
+
"ChartType", // ChartType An enumeration of the possible chart types.
|
|
23
|
+
"ChartHiddenDimensionStrategy",
|
|
24
|
+
"ChartMergeStrategy",
|
|
25
|
+
"CurveStyle",
|
|
26
|
+
"PointStyle",
|
|
27
|
+
"Position"
|
|
23
28
|
];
|
|
24
29
|
|
|
25
30
|
// import all known enums as props of chartsapp
|
|
@@ -14,14 +14,33 @@ export const getFilesIterator = ({
|
|
|
14
14
|
qob,
|
|
15
15
|
parentId = null,
|
|
16
16
|
folderTypes,
|
|
17
|
-
fileTypes
|
|
17
|
+
fileTypes,
|
|
18
|
+
token
|
|
18
19
|
}) => {
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
// parentId can be null to search everywhere
|
|
22
23
|
if (!is.null(parentId)) assert.nonEmptyString(parentId)
|
|
23
|
-
assert.boolean(folderTypes)
|
|
24
|
-
assert.boolean(fileTypes)
|
|
24
|
+
assert.boolean(folderTypes || !!token)
|
|
25
|
+
assert.boolean(fileTypes || !!token)
|
|
26
|
+
|
|
27
|
+
let state = {
|
|
28
|
+
qob,
|
|
29
|
+
parentId,
|
|
30
|
+
folderTypes,
|
|
31
|
+
fileTypes,
|
|
32
|
+
pageToken: null,
|
|
33
|
+
tank: []
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (token) {
|
|
37
|
+
const saved = JSON.parse(Buffer.from(token, 'base64').toString())
|
|
38
|
+
state = { ...state, ...saved }
|
|
39
|
+
qob = state.qob
|
|
40
|
+
parentId = state.parentId
|
|
41
|
+
folderTypes = state.folderTypes
|
|
42
|
+
fileTypes = state.fileTypes
|
|
43
|
+
}
|
|
25
44
|
|
|
26
45
|
// DriveApp doesnt give option to specify these so this will be fixed
|
|
27
46
|
const fields = `files(${minFields}),nextPageToken`
|
|
@@ -49,9 +68,9 @@ export const getFilesIterator = ({
|
|
|
49
68
|
*/
|
|
50
69
|
function* filesink() {
|
|
51
70
|
// the result tank
|
|
52
|
-
let tank = []
|
|
71
|
+
let tank = [...state.tank]
|
|
53
72
|
// the next page token
|
|
54
|
-
let pageToken =
|
|
73
|
+
let pageToken = state.pageToken
|
|
55
74
|
|
|
56
75
|
do {
|
|
57
76
|
// if nothing in the tank, fill it up
|
|
@@ -63,10 +82,14 @@ export const getFilesIterator = ({
|
|
|
63
82
|
// format the results into the folder or file object
|
|
64
83
|
assert.array(data.files)
|
|
65
84
|
assert.function(DriveApp.__settleClass)
|
|
66
|
-
|
|
85
|
+
|
|
86
|
+
// we store raw data in state for continuation
|
|
87
|
+
state.tank = data.files
|
|
88
|
+
tank = [...state.tank]
|
|
67
89
|
|
|
68
90
|
// the presence of a nextPageToken is the signal that there's more to come
|
|
69
91
|
pageToken = data.nextPageToken
|
|
92
|
+
state.pageToken = pageToken
|
|
70
93
|
|
|
71
94
|
// if we still have nothing in the tank but there's a page token, keep going
|
|
72
95
|
if (!tank.length && !pageToken) break;
|
|
@@ -74,7 +97,12 @@ export const getFilesIterator = ({
|
|
|
74
97
|
|
|
75
98
|
// if we've got anything in the tank send back the oldest one
|
|
76
99
|
if (tank.length) {
|
|
77
|
-
|
|
100
|
+
const raw = tank.splice(0, 1)[0]
|
|
101
|
+
state.tank.splice(0, 1)
|
|
102
|
+
yield {
|
|
103
|
+
__fakeResolved: DriveApp.__settleClass(raw),
|
|
104
|
+
__fakeRaw: raw
|
|
105
|
+
}
|
|
78
106
|
}
|
|
79
107
|
|
|
80
108
|
// if there's still anything left in the tank,
|
|
@@ -85,9 +113,26 @@ export const getFilesIterator = ({
|
|
|
85
113
|
// create the iterator
|
|
86
114
|
const fileit = filesink()
|
|
87
115
|
|
|
116
|
+
const continuationHandler = (peeked) => {
|
|
117
|
+
// if we've peeked, we need to make sure the peeked item is included in the tank
|
|
118
|
+
// because GAS continuation tokens should represent the state AFTER the last next() call.
|
|
119
|
+
// Peeker already called generator.next(), so the item to be returned by the NEXT next() call
|
|
120
|
+
// is in peeked.value.
|
|
121
|
+
const tank = [...state.tank]
|
|
122
|
+
if (peeked && !peeked.done) {
|
|
123
|
+
// Put the pre-fetched item back at the start of the tank for the token
|
|
124
|
+
tank.unshift(peeked.value.__fakeRaw || peeked.value)
|
|
125
|
+
}
|
|
126
|
+
const tokenState = {
|
|
127
|
+
...state,
|
|
128
|
+
tank
|
|
129
|
+
}
|
|
130
|
+
return Buffer.from(JSON.stringify(tokenState)).toString('base64')
|
|
131
|
+
}
|
|
132
|
+
|
|
88
133
|
// a regular iterator doesnt support the same methods
|
|
89
134
|
// as Apps Script so we'll fake that too
|
|
90
|
-
return newPeeker(fileit)
|
|
135
|
+
return newPeeker(fileit, continuationHandler)
|
|
91
136
|
|
|
92
137
|
}
|
|
93
138
|
|
|
@@ -5,6 +5,8 @@ import { notYetImplemented, isFolder } from '../../support/helpers.js'
|
|
|
5
5
|
import { Proxies } from '../../support/proxies.js'
|
|
6
6
|
import { Utils } from '../../support/utils.js'
|
|
7
7
|
import { Access, Permission } from '../enums/driveenums.js'
|
|
8
|
+
import { getFilesIterator } from './driveiterators.js'
|
|
9
|
+
import { Syncit } from '../../support/syncit.js'
|
|
8
10
|
const { is } = Utils
|
|
9
11
|
|
|
10
12
|
|
|
@@ -19,6 +21,7 @@ export class FakeDriveApp {
|
|
|
19
21
|
this.__rootFolder = null
|
|
20
22
|
this.folderApp = newFakeFolderApp()
|
|
21
23
|
this.__settleClass = (file) => isFolder(file) ? newFakeDriveFolder(file) : newFakeDriveFile(file)
|
|
24
|
+
this.__enforceSingleParent = true
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
|
|
@@ -133,37 +136,68 @@ export class FakeDriveApp {
|
|
|
133
136
|
return this.getRootFolder().createFolder(name)
|
|
134
137
|
}
|
|
135
138
|
|
|
136
|
-
|
|
139
|
+
createShortcut(targetId, resourceKey) {
|
|
140
|
+
return this.getRootFolder().createShortcut(targetId, resourceKey)
|
|
141
|
+
}
|
|
137
142
|
|
|
138
|
-
|
|
139
|
-
return
|
|
143
|
+
createShortcutForTargetIdAndResourceKey(targetId, resourceKey) {
|
|
144
|
+
return this.getRootFolder().createShortcutForTargetIdAndResourceKey(targetId, resourceKey)
|
|
140
145
|
}
|
|
141
|
-
|
|
142
|
-
|
|
146
|
+
|
|
147
|
+
getFolderByIdAndResourceKey(id, resourceKey) {
|
|
148
|
+
return this.getFolderById(id)
|
|
143
149
|
}
|
|
144
150
|
|
|
145
|
-
|
|
146
|
-
return
|
|
151
|
+
getFileByIdAndResourceKey(id, resourceKey) {
|
|
152
|
+
return this.getFileById(id)
|
|
147
153
|
}
|
|
148
|
-
|
|
149
|
-
|
|
154
|
+
|
|
155
|
+
continueFileIterator(token) {
|
|
156
|
+
return getFilesIterator({ token })
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
continueFolderIterator(token) {
|
|
160
|
+
return getFilesIterator({ token })
|
|
150
161
|
}
|
|
162
|
+
|
|
151
163
|
getTrashedFiles() {
|
|
152
|
-
return
|
|
164
|
+
return getFilesIterator({
|
|
165
|
+
folderTypes: false,
|
|
166
|
+
fileTypes: true,
|
|
167
|
+
qob: ['trashed = true']
|
|
168
|
+
})
|
|
153
169
|
}
|
|
170
|
+
|
|
154
171
|
getTrashedFolders() {
|
|
155
|
-
return
|
|
172
|
+
return getFilesIterator({
|
|
173
|
+
folderTypes: true,
|
|
174
|
+
fileTypes: false,
|
|
175
|
+
qob: ['trashed = true']
|
|
176
|
+
})
|
|
156
177
|
}
|
|
157
178
|
|
|
158
179
|
getStorageLimit() {
|
|
159
|
-
|
|
180
|
+
const { data } = Syncit.fxDrive({
|
|
181
|
+
prop: 'about',
|
|
182
|
+
method: 'get',
|
|
183
|
+
params: { fields: 'storageQuota' }
|
|
184
|
+
})
|
|
185
|
+
return parseInt(data.storageQuota.limit, 10)
|
|
160
186
|
}
|
|
187
|
+
|
|
161
188
|
getStorageUsed() {
|
|
162
|
-
|
|
189
|
+
const { data } = Syncit.fxDrive({
|
|
190
|
+
prop: 'about',
|
|
191
|
+
method: 'get',
|
|
192
|
+
params: { fields: 'storageQuota' }
|
|
193
|
+
})
|
|
194
|
+
return parseInt(data.storageQuota.usage, 10)
|
|
163
195
|
}
|
|
164
|
-
|
|
165
|
-
|
|
196
|
+
|
|
197
|
+
enforceSingleParent(enabled) {
|
|
198
|
+
this.__enforceSingleParent = enabled
|
|
166
199
|
}
|
|
200
|
+
|
|
167
201
|
get Access() {
|
|
168
202
|
return Access
|
|
169
203
|
}
|
|
@@ -68,6 +68,96 @@ class FakeDriveFile extends FakeDriveMeta {
|
|
|
68
68
|
return this.__getDecorated("webContentLink")
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* get as a blob
|
|
73
|
+
* @param {string} contentType
|
|
74
|
+
* @returns {FakeBlob}
|
|
75
|
+
*/
|
|
76
|
+
getAs(contentType) {
|
|
77
|
+
if (contentType === this.getMimeType()) {
|
|
78
|
+
return this.getBlob()
|
|
79
|
+
}
|
|
80
|
+
const result = Syncit.fxDriveExport({ id: this.getId(), mimeType: contentType })
|
|
81
|
+
if (result.error) {
|
|
82
|
+
// The error might be a string (from catch in sxStreamer) or an object
|
|
83
|
+
let isNotExportable = false;
|
|
84
|
+
let message = result.error;
|
|
85
|
+
if (typeof result.error === 'string' && result.error.startsWith('{')) {
|
|
86
|
+
try {
|
|
87
|
+
const parsed = JSON.parse(result.error);
|
|
88
|
+
isNotExportable = parsed.error?.errors?.[0]?.reason === 'fileNotExportable' ||
|
|
89
|
+
parsed.error?.reason === 'fileNotExportable';
|
|
90
|
+
message = parsed.error?.message || parsed.message || result.error;
|
|
91
|
+
} catch (e) {
|
|
92
|
+
// ignore
|
|
93
|
+
}
|
|
94
|
+
} else if (typeof result.error === 'object') {
|
|
95
|
+
isNotExportable = result.error.error?.errors?.[0]?.reason === 'fileNotExportable' ||
|
|
96
|
+
result.error.errors?.[0]?.reason === 'fileNotExportable' ||
|
|
97
|
+
result.error.error?.reason === 'fileNotExportable' ||
|
|
98
|
+
result.error.reason === 'fileNotExportable';
|
|
99
|
+
message = result.error.error?.message || result.error.message || JSON.stringify(result.error);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Live GAS automatically handles exporting plain text/images to PDF etc.
|
|
103
|
+
// The REST API doesn't support this directly. We workaround it by
|
|
104
|
+
// temporarily converting the file to a Google Doc, exporting that, and trashing it.
|
|
105
|
+
if (isNotExportable) {
|
|
106
|
+
let targetMimeType = 'application/vnd.google-apps.document';
|
|
107
|
+
const currentMime = this.getMimeType();
|
|
108
|
+
if (currentMime === 'text/csv' || currentMime === 'text/tab-separated-values' || currentMime === 'application/vnd.ms-excel' || currentMime === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') {
|
|
109
|
+
targetMimeType = 'application/vnd.google-apps.spreadsheet';
|
|
110
|
+
} else if (currentMime === 'application/vnd.ms-powerpoint' || currentMime === 'application/vnd.openxmlformats-officedocument.presentationml.presentation') {
|
|
111
|
+
targetMimeType = 'application/vnd.google-apps.presentation';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
// 1. Copy to temp Google Doc format
|
|
116
|
+
const copyResult = Syncit.fxDrive({
|
|
117
|
+
prop: 'files',
|
|
118
|
+
method: 'copy',
|
|
119
|
+
params: {
|
|
120
|
+
fileId: this.getId(),
|
|
121
|
+
resource: {
|
|
122
|
+
mimeType: targetMimeType,
|
|
123
|
+
name: `Temp_gasfakes_conversion_${this.getName()}`
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (!copyResult.data || !copyResult.data.id) {
|
|
129
|
+
throw new Error(`Failed to copy to intermediate format: ${JSON.stringify(copyResult.response)}`);
|
|
130
|
+
}
|
|
131
|
+
const tempFileId = copyResult.data.id;
|
|
132
|
+
|
|
133
|
+
// 2. Export the temp file
|
|
134
|
+
const tempExportResult = Syncit.fxDriveExport({ id: tempFileId, mimeType: contentType });
|
|
135
|
+
|
|
136
|
+
// 3. Delete the temp file
|
|
137
|
+
Syncit.fxDrive({
|
|
138
|
+
prop: 'files',
|
|
139
|
+
method: 'update',
|
|
140
|
+
params: {
|
|
141
|
+
fileId: tempFileId,
|
|
142
|
+
resource: { trashed: true }
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (tempExportResult.error) {
|
|
147
|
+
throw new Error(tempExportResult.error.error?.message || tempExportResult.error.message || JSON.stringify(tempExportResult.error));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return Utilities.newBlob(tempExportResult.data, contentType, this.getName());
|
|
151
|
+
} catch (workaroundError) {
|
|
152
|
+
throw new Error(`getAs API returned: ${message}. Then, a temporary two-step conversion workaround failed: ${workaroundError.message}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
throw new Error(message)
|
|
157
|
+
}
|
|
158
|
+
return Utilities.newBlob(result.data, contentType, this.getName())
|
|
159
|
+
}
|
|
160
|
+
|
|
71
161
|
/**
|
|
72
162
|
* set the content to something else
|
|
73
163
|
* @param {string} content apparently this can only be a string and not a blob
|
|
@@ -151,6 +241,21 @@ class FakeDriveFile extends FakeDriveMeta {
|
|
|
151
241
|
return newFakeDriveFile(data);
|
|
152
242
|
}
|
|
153
243
|
|
|
244
|
+
getTargetId() {
|
|
245
|
+
this.__decorateWithFields("shortcutDetails")
|
|
246
|
+
return this.meta.shortcutDetails?.targetId || null
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
getTargetMimeType() {
|
|
250
|
+
this.__decorateWithFields("shortcutDetails")
|
|
251
|
+
return this.meta.shortcutDetails?.targetMimeType || null
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
getTargetResourceKey() {
|
|
255
|
+
this.__decorateWithFields("shortcutDetails")
|
|
256
|
+
return this.meta.shortcutDetails?.targetResourceKey || null
|
|
257
|
+
}
|
|
258
|
+
|
|
154
259
|
}
|
|
155
260
|
|
|
156
261
|
/**
|
|
@@ -58,6 +58,14 @@ export class FakeDriveFolder extends FakeDriveMeta {
|
|
|
58
58
|
})
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
createShortcut(targetId, resourceKey) {
|
|
62
|
+
return this.folderApp.createShortcut({ targetId, resourceKey, file: { parents: [this.getId()] } })
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
createShortcutForTargetIdAndResourceKey(targetId, resourceKey) {
|
|
66
|
+
return this.folderApp.createShortcutForTargetIdAndResourceKey({ targetId, resourceKey, file: { parents: [this.getId()] } })
|
|
67
|
+
}
|
|
68
|
+
|
|
61
69
|
/**
|
|
62
70
|
* get files in this folder
|
|
63
71
|
* @return {FakeDriveFileIterator}
|