sourcemonkey 0.0.0-stage → 0.5.0
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/LICENSE +21 -0
- package/README.md +87 -2
- package/dist/bytes.js +37 -0
- package/dist/channel-placeholder.js +5 -0
- package/dist/collection-scan.js +17 -0
- package/dist/collections.html +353 -0
- package/dist/compare.html +358 -0
- package/dist/composed-source.js +205 -0
- package/dist/connect-prompt.html +47 -0
- package/dist/debug/icons.html +211 -0
- package/dist/debug/storage.html +163 -0
- package/dist/debug-log.js +75 -0
- package/dist/diagnostic-location.js +64 -0
- package/dist/eval-hook.js +308 -0
- package/dist/extension/background.js +5263 -0
- package/dist/extension/channel.js +133 -0
- package/dist/extension/code-links.js +187 -0
- package/dist/extension/code-tabs.js +460 -0
- package/dist/extension/collection-dialog.js +619 -0
- package/dist/extension/collection-display.js +322 -0
- package/dist/extension/collections.js +2905 -0
- package/dist/extension/compare.js +867 -0
- package/dist/extension/concurrency.js +186 -0
- package/dist/extension/connect-grants.js +109 -0
- package/dist/extension/connect-prompt-page.js +70 -0
- package/dist/extension/connect-prompt.js +266 -0
- package/dist/extension/copy-button.js +63 -0
- package/dist/extension/copy-dialog.js +502 -0
- package/dist/extension/copy-preference.js +66 -0
- package/dist/extension/debug/icons.js +203 -0
- package/dist/extension/debug/storage.js +365 -0
- package/dist/extension/deep-link.js +77 -0
- package/dist/extension/delete-dialog.js +432 -0
- package/dist/extension/delete-icon.js +40 -0
- package/dist/extension/details-tooltips.js +66 -0
- package/dist/extension/directories.js +679 -0
- package/dist/extension/export-backup.js +407 -0
- package/dist/extension/export-dialog.js +142 -0
- package/dist/extension/file-access.js +74 -0
- package/dist/extension/file-changes.js +346 -0
- package/dist/extension/frame-info.js +85 -0
- package/dist/extension/github-provider.js +336 -0
- package/dist/extension/glyph.js +84 -0
- package/dist/extension/gm-bindings.js +601 -0
- package/dist/extension/gm-download.js +419 -0
- package/dist/extension/gm-menu.js +228 -0
- package/dist/extension/gm-notification.js +141 -0
- package/dist/extension/gm-originator-map.js +23 -0
- package/dist/extension/gm-tab-data.js +121 -0
- package/dist/extension/gm-tab.js +160 -0
- package/dist/extension/gm-url-change.js +111 -0
- package/dist/extension/gm-url-scheme.js +27 -0
- package/dist/extension/gm-values.js +176 -0
- package/dist/extension/gm-window-control.js +104 -0
- package/dist/extension/gm-xhr-headers.js +333 -0
- package/dist/extension/gm-xhr.js +584 -0
- package/dist/extension/injector.js +3145 -0
- package/dist/extension/install-capture.js +277 -0
- package/dist/extension/line-diff.js +233 -0
- package/dist/extension/local-copy.js +560 -0
- package/dist/extension/local-delete.js +276 -0
- package/dist/extension/location-kinds.js +108 -0
- package/dist/extension/log-format.js +158 -0
- package/dist/extension/managed-changes.js +361 -0
- package/dist/extension/managed-items.js +57 -0
- package/dist/extension/match-index.js +375 -0
- package/dist/extension/missing-scripts.js +66 -0
- package/dist/extension/more-menu.js +109 -0
- package/dist/extension/move-dialog.js +197 -0
- package/dist/extension/offscreen.js +37 -0
- package/dist/extension/options.js +504 -0
- package/dist/extension/page-heading.js +170 -0
- package/dist/extension/page-links.js +199 -0
- package/dist/extension/parse-check-client.js +70 -0
- package/dist/extension/parse-check.js +161 -0
- package/dist/extension/parse-sandbox-host.js +172 -0
- package/dist/extension/parse-sandbox.js +175 -0
- package/dist/extension/popup-commands-open.js +41 -0
- package/dist/extension/popup-view.js +387 -0
- package/dist/extension/popup.js +703 -0
- package/dist/extension/prism-userscript.js +53 -0
- package/dist/extension/refresh-error.js +136 -0
- package/dist/extension/refresh-targets.js +160 -0
- package/dist/extension/related-versions.js +1144 -0
- package/dist/extension/resize-observer-loop.js +19 -0
- package/dist/extension/row-cache.js +387 -0
- package/dist/extension/script-blobs.js +413 -0
- package/dist/extension/script-details.js +658 -0
- package/dist/extension/script-enablement.js +285 -0
- package/dist/extension/script-identity.js +310 -0
- package/dist/extension/script-keys.js +54 -0
- package/dist/extension/script-log-id.js +155 -0
- package/dist/extension/script-log.js +290 -0
- package/dist/extension/script-rejection.js +104 -0
- package/dist/extension/script-sites.js +158 -0
- package/dist/extension/script-store.js +3507 -0
- package/dist/extension/script-updater.js +991 -0
- package/dist/extension/script-url-hash.js +61 -0
- package/dist/extension/select-all.js +37 -0
- package/dist/extension/select-open-guard.js +90 -0
- package/dist/extension/site-icon.js +1261 -0
- package/dist/extension/sm-dev-link.js +302 -0
- package/dist/extension/solo-click.js +58 -0
- package/dist/extension/sticky-numbers.js +188 -0
- package/dist/extension/storage-breakdown.js +253 -0
- package/dist/extension/storage-scan.js +86 -0
- package/dist/extension/storage-usage.js +194 -0
- package/dist/extension/structural-keys.js +29 -0
- package/dist/extension/sw-teardown.js +36 -0
- package/dist/extension/tab-status.js +403 -0
- package/dist/extension/toolbar-summary.js +105 -0
- package/dist/extension/user-scripts-access.js +43 -0
- package/dist/extension/userscript.js +3672 -0
- package/dist/extension/value-store.js +204 -0
- package/dist/extension/version-probe-fetch.js +68 -0
- package/dist/extension/worker-client.js +109 -0
- package/dist/extension/worker-ops.js +171 -0
- package/dist/extension/zip-writer.js +24 -0
- package/dist/external-files.js +878 -0
- package/dist/file-loader.js +436 -0
- package/dist/file-locations.js +439 -0
- package/dist/generated-wrappers.js +5 -0
- package/dist/github-auth.js +148 -0
- package/dist/github-urls.js +46 -0
- package/dist/gm-api-catalog.js +217 -0
- package/dist/gm-connect.js +273 -0
- package/dist/gm-xhr-types.js +5 -0
- package/dist/http-read.js +142 -0
- package/dist/icons/icon-128.png +0 -0
- package/dist/icons/icon-16.png +0 -0
- package/dist/icons/icon-48.png +0 -0
- package/dist/local-file-overrides.js +558 -0
- package/dist/manifest.json +38 -0
- package/dist/markup-response.js +126 -0
- package/dist/match-targets.js +305 -0
- package/dist/menu-captions.js +22 -0
- package/dist/offscreen.html +39 -0
- package/dist/options.html +197 -0
- package/dist/parse-check-protocol.js +51 -0
- package/dist/parse-location.js +171 -0
- package/dist/parse-sandbox.html +56 -0
- package/dist/phase-timing.js +155 -0
- package/dist/popup.html +197 -0
- package/dist/public-suffixes.js +6957 -0
- package/dist/run-context.js +31 -0
- package/dist/runtime-messages.js +34 -0
- package/dist/sandboxed-page.js +35 -0
- package/dist/script-build.js +787 -0
- package/dist/script-display.js +383 -0
- package/dist/script-icon.js +43 -0
- package/dist/script-links.js +204 -0
- package/dist/script-source-url.js +214 -0
- package/dist/script-world.js +65 -0
- package/dist/sm-dev-protocol.js +84 -0
- package/dist/source-map.js +177 -0
- package/dist/stack-link.js +138 -0
- package/dist/styles.css +825 -0
- package/dist/unsafe-window-use.js +229 -0
- package/dist/url-match.js +734 -0
- package/dist/userscript-header.js +248 -0
- package/dist/userscript.html +1165 -0
- package/dist/vendor/fflate.LICENSE +21 -0
- package/dist/vendor/fflate.js +2692 -0
- package/dist/vendor/prism/prism-line-numbers.css +1 -0
- package/dist/vendor/prism/prism-line-numbers.js +1 -0
- package/dist/vendor/prism/prism.css +1 -0
- package/dist/vendor/prism/prism.js +1946 -0
- package/dist/version-compare.js +141 -0
- package/dist/version-probe.js +120 -0
- package/lib/build-info.json +1 -0
- package/lib/bytes.d.ts +3 -0
- package/lib/bytes.js +37 -0
- package/lib/channel-placeholder.d.ts +1 -0
- package/lib/channel-placeholder.js +5 -0
- package/lib/collection-scan.d.ts +8 -0
- package/lib/collection-scan.js +17 -0
- package/lib/composed-source.d.ts +28 -0
- package/lib/composed-source.js +205 -0
- package/lib/debug-log.d.ts +9 -0
- package/lib/debug-log.js +75 -0
- package/lib/diagnostic-location.d.ts +8 -0
- package/lib/diagnostic-location.js +64 -0
- package/lib/eval-hook.d.ts +34 -0
- package/lib/eval-hook.js +308 -0
- package/lib/external-files.d.ts +111 -0
- package/lib/external-files.js +878 -0
- package/lib/file-loader.d.ts +48 -0
- package/lib/file-loader.js +436 -0
- package/lib/file-locations.d.ts +44 -0
- package/lib/file-locations.js +439 -0
- package/lib/generated-wrappers.d.ts +11 -0
- package/lib/generated-wrappers.js +5 -0
- package/lib/github-auth.d.ts +12 -0
- package/lib/github-auth.js +148 -0
- package/lib/github-urls.d.ts +1 -0
- package/lib/github-urls.js +46 -0
- package/lib/gm-api-catalog.d.ts +24 -0
- package/lib/gm-api-catalog.js +217 -0
- package/lib/gm-connect.d.ts +34 -0
- package/lib/gm-connect.js +273 -0
- package/lib/gm-xhr-types.d.ts +68 -0
- package/lib/gm-xhr-types.js +5 -0
- package/lib/harness/browser-extensions.d.ts +73 -0
- package/lib/harness/browser-extensions.js +285 -0
- package/lib/harness/browser-lang.d.ts +20 -0
- package/lib/harness/browser-lang.js +86 -0
- package/lib/harness/browser-launch.d.ts +14 -0
- package/lib/harness/browser-launch.js +184 -0
- package/lib/harness/checks.d.ts +110 -0
- package/lib/harness/checks.js +321 -0
- package/lib/harness/cli.d.ts +2 -0
- package/lib/harness/cli.js +2259 -0
- package/lib/harness/command-args.d.ts +91 -0
- package/lib/harness/command-args.js +369 -0
- package/lib/harness/command-lines.d.ts +80 -0
- package/lib/harness/command-lines.js +180 -0
- package/lib/harness/command-list.d.ts +206 -0
- package/lib/harness/command-list.js +1475 -0
- package/lib/harness/console-commands.d.ts +463 -0
- package/lib/harness/console-commands.js +3216 -0
- package/lib/harness/download-watch.d.ts +21 -0
- package/lib/harness/download-watch.js +156 -0
- package/lib/harness/extension-backend.d.ts +41 -0
- package/lib/harness/extension-backend.js +100 -0
- package/lib/harness/extension-host.d.ts +112 -0
- package/lib/harness/extension-host.js +552 -0
- package/lib/harness/extension-launch.d.ts +67 -0
- package/lib/harness/extension-launch.js +285 -0
- package/lib/harness/extension-link.d.ts +315 -0
- package/lib/harness/extension-link.js +607 -0
- package/lib/harness/external-manager.d.ts +146 -0
- package/lib/harness/external-manager.js +414 -0
- package/lib/harness/fixture.d.ts +165 -0
- package/lib/harness/fixture.js +461 -0
- package/lib/harness/frame-sessions.d.ts +71 -0
- package/lib/harness/frame-sessions.js +248 -0
- package/lib/harness/frames.d.ts +68 -0
- package/lib/harness/frames.js +216 -0
- package/lib/harness/help-format.d.ts +17 -0
- package/lib/harness/help-format.js +127 -0
- package/lib/harness/host.d.ts +352 -0
- package/lib/harness/host.js +423 -0
- package/lib/harness/index.d.ts +8 -0
- package/lib/harness/index.js +10 -0
- package/lib/harness/input-file.d.ts +18 -0
- package/lib/harness/input-file.js +61 -0
- package/lib/harness/json-output.d.ts +565 -0
- package/lib/harness/json-output.js +71 -0
- package/lib/harness/manager-actions.d.ts +21 -0
- package/lib/harness/manager-actions.js +129 -0
- package/lib/harness/manager-commands.d.ts +142 -0
- package/lib/harness/manager-commands.js +1196 -0
- package/lib/harness/manager-fetch.d.ts +33 -0
- package/lib/harness/manager-fetch.js +239 -0
- package/lib/harness/managers.d.ts +24 -0
- package/lib/harness/managers.js +27 -0
- package/lib/harness/output-style.d.ts +125 -0
- package/lib/harness/output-style.js +322 -0
- package/lib/harness/page-commands.d.ts +134 -0
- package/lib/harness/page-commands.js +1141 -0
- package/lib/harness/page-output.d.ts +67 -0
- package/lib/harness/page-output.js +139 -0
- package/lib/harness/page-watch.d.ts +171 -0
- package/lib/harness/page-watch.js +607 -0
- package/lib/harness/parse-check-node.d.ts +3 -0
- package/lib/harness/parse-check-node.js +132 -0
- package/lib/harness/path-args.d.ts +22 -0
- package/lib/harness/path-args.js +148 -0
- package/lib/harness/playwright-chromium.d.ts +16 -0
- package/lib/harness/playwright-chromium.js +86 -0
- package/lib/harness/prepare.d.ts +168 -0
- package/lib/harness/prepare.js +364 -0
- package/lib/harness/prompt-editor.d.ts +17 -0
- package/lib/harness/prompt-editor.js +354 -0
- package/lib/harness/prompt-history.d.ts +13 -0
- package/lib/harness/prompt-history.js +89 -0
- package/lib/harness/rebuild-advice.d.ts +6 -0
- package/lib/harness/rebuild-advice.js +39 -0
- package/lib/harness/run-log.d.ts +47 -0
- package/lib/harness/run-log.js +140 -0
- package/lib/harness/run-logs.d.ts +132 -0
- package/lib/harness/run-logs.js +241 -0
- package/lib/harness/run.d.ts +194 -0
- package/lib/harness/run.js +1973 -0
- package/lib/harness/runs.d.ts +229 -0
- package/lib/harness/runs.js +726 -0
- package/lib/harness/save-html.d.ts +39 -0
- package/lib/harness/save-html.js +89 -0
- package/lib/harness/saved-pages.d.ts +120 -0
- package/lib/harness/saved-pages.js +275 -0
- package/lib/harness/script-mark.d.ts +17 -0
- package/lib/harness/script-mark.js +56 -0
- package/lib/harness/snapshot.d.ts +143 -0
- package/lib/harness/snapshot.js +474 -0
- package/lib/harness/stuck-tabs.d.ts +19 -0
- package/lib/harness/stuck-tabs.js +131 -0
- package/lib/harness/tab-refs.d.ts +20 -0
- package/lib/harness/tab-refs.js +80 -0
- package/lib/harness/tampermonkey.d.ts +71 -0
- package/lib/harness/tampermonkey.js +356 -0
- package/lib/harness/tm-ids.d.ts +18 -0
- package/lib/harness/tm-ids.js +88 -0
- package/lib/harness/violentmonkey.d.ts +34 -0
- package/lib/harness/violentmonkey.js +162 -0
- package/lib/harness/wait.d.ts +77 -0
- package/lib/harness/wait.js +458 -0
- package/lib/http-read.d.ts +30 -0
- package/lib/http-read.js +142 -0
- package/lib/local-file-overrides.d.ts +35 -0
- package/lib/local-file-overrides.js +558 -0
- package/lib/manifest.json +38 -0
- package/lib/markup-response.d.ts +11 -0
- package/lib/markup-response.js +126 -0
- package/lib/match-targets.d.ts +18 -0
- package/lib/match-targets.js +305 -0
- package/lib/menu-captions.d.ts +1 -0
- package/lib/menu-captions.js +22 -0
- package/lib/parse-check-protocol.d.ts +27 -0
- package/lib/parse-check-protocol.js +51 -0
- package/lib/parse-location.d.ts +12 -0
- package/lib/parse-location.js +171 -0
- package/lib/phase-timing.d.ts +12 -0
- package/lib/phase-timing.js +155 -0
- package/lib/public-suffixes.d.ts +1 -0
- package/lib/public-suffixes.js +6957 -0
- package/lib/run-context.d.ts +2 -0
- package/lib/run-context.js +31 -0
- package/lib/runtime-messages.d.ts +10 -0
- package/lib/runtime-messages.js +34 -0
- package/lib/sandboxed-page.d.ts +5 -0
- package/lib/sandboxed-page.js +35 -0
- package/lib/script-build.d.ts +111 -0
- package/lib/script-build.js +787 -0
- package/lib/script-display.d.ts +51 -0
- package/lib/script-display.js +383 -0
- package/lib/script-icon.d.ts +16 -0
- package/lib/script-icon.js +43 -0
- package/lib/script-links.d.ts +24 -0
- package/lib/script-links.js +204 -0
- package/lib/script-source-url.d.ts +8 -0
- package/lib/script-source-url.js +214 -0
- package/lib/script-world.d.ts +6 -0
- package/lib/script-world.js +65 -0
- package/lib/sm-dev-protocol.d.ts +251 -0
- package/lib/sm-dev-protocol.js +84 -0
- package/lib/source-map.d.ts +32 -0
- package/lib/source-map.js +177 -0
- package/lib/stack-link.d.ts +15 -0
- package/lib/stack-link.js +138 -0
- package/lib/unsafe-window-use.d.ts +1 -0
- package/lib/unsafe-window-use.js +229 -0
- package/lib/url-match.d.ts +40 -0
- package/lib/url-match.js +734 -0
- package/lib/userscript-header.d.ts +33 -0
- package/lib/userscript-header.js +248 -0
- package/lib/version-compare.d.ts +6 -0
- package/lib/version-compare.js +141 -0
- package/lib/version-probe.d.ts +17 -0
- package/lib/version-probe.js +120 -0
- package/package.json +55 -3
- package/skills/SourceMonkey-dev/SKILL.md +868 -0
- package/skills/SourceMonkey-dev/references/cdp.md +293 -0
- package/skills/SourceMonkey-dev/references/commands.md +297 -0
- package/skills/SourceMonkey-dev/references/other-managers.md +63 -0
- package/skills/SourceMonkey-dev/references/specs.md +46 -0
- package/skills/SourceMonkey-friction/SKILL.md +103 -0
- package/skills/SourceMonkey-friction/references/template.md +59 -0
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
// Making an editable copy of an installed script on local disk.
|
|
2
|
+
//
|
|
3
|
+
// The copy is written into the "Scripts copied in Downloads" directory
|
|
4
|
+
// collection — `<Downloads>/SourceMonkey/` — through `chrome.downloads`,
|
|
5
|
+
// the one write path an extension has that needs no dialog and reports
|
|
6
|
+
// back the absolute path of what it wrote. Everything else (the
|
|
7
|
+
// dialog, the enable sweep, the navigation) lives in `copy-dialog.ts`
|
|
8
|
+
// and `userscript.ts`; this module is the pure logic plus the write.
|
|
9
|
+
//
|
|
10
|
+
// See docs/local-copy.md.
|
|
11
|
+
import { base64ToBytes } from '../bytes.js';
|
|
12
|
+
import { assertWorkerContext } from '../run-context.js';
|
|
13
|
+
import { MAX_SCAN_DEPTH } from '../collection-scan.js';
|
|
14
|
+
import { collectionNameInUse } from './collection-display.js';
|
|
15
|
+
import { canonicalCollectionInput, isHttpUrl, baseName, joinPath, pathToFileUrl, statFromListing, } from '../file-locations.js';
|
|
16
|
+
import { localFileCandidates } from '../local-file-overrides.js';
|
|
17
|
+
import { scriptBasename } from '../script-display.js';
|
|
18
|
+
import { locationKey } from './script-identity.js';
|
|
19
|
+
import { dlog } from '../debug-log.js';
|
|
20
|
+
import { createCollection, describeError, emitInitialLoadLog, getCollections, refreshCollection, setScriptsEnabled, withoutBodies, } from './script-store.js';
|
|
21
|
+
import { callWorker } from './worker-client.js';
|
|
22
|
+
import { joinFullSource, splitHeaderBody } from '../userscript-header.js';
|
|
23
|
+
// ---------- The default collection and where Downloads is ----------
|
|
24
|
+
// The subdirectory of the browser's Downloads folder every copy lands
|
|
25
|
+
// in. Also the directory the default collection is created at.
|
|
26
|
+
export const DOWNLOADS_COPIES_SUBDIR = 'SourceMonkey';
|
|
27
|
+
// The default collection's seeded name. The user's to edit — the
|
|
28
|
+
// collection is found by the id recorded below, never by this.
|
|
29
|
+
export const DOWNLOADS_COPIES_NAME = 'Scripts copied in Downloads';
|
|
30
|
+
// `chrome.storage.local` key. Both values are learned from the first
|
|
31
|
+
// completed copy: Chrome exposes no API for where Downloads is, but a
|
|
32
|
+
// finished download reports its absolute path, and ours was written to
|
|
33
|
+
// a known relative name under it.
|
|
34
|
+
export const DOWNLOADS_COPIES_KEY = 'smDownloadsCopies';
|
|
35
|
+
export async function readDownloadsCopiesSetting() {
|
|
36
|
+
const got = await chrome.storage.local.get(DOWNLOADS_COPIES_KEY);
|
|
37
|
+
return parseDownloadsCopiesSetting(got[DOWNLOADS_COPIES_KEY]);
|
|
38
|
+
}
|
|
39
|
+
export function parseDownloadsCopiesSetting(raw) {
|
|
40
|
+
if (!raw || typeof raw !== 'object')
|
|
41
|
+
return null;
|
|
42
|
+
const { downloadsDir, collectionId } = raw;
|
|
43
|
+
if (typeof downloadsDir !== 'string' || !downloadsDir)
|
|
44
|
+
return null;
|
|
45
|
+
return {
|
|
46
|
+
downloadsDir,
|
|
47
|
+
collectionId: typeof collectionId === 'string' && collectionId ? collectionId : null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// Worker side (`ensureDownloadsCollection`). A page writes it through
|
|
51
|
+
// worker-client.ts's `writeSetting`.
|
|
52
|
+
async function writeDownloadsCopiesSetting(setting) {
|
|
53
|
+
assertWorkerContext('write storage');
|
|
54
|
+
await chrome.storage.local.set({ [DOWNLOADS_COPIES_KEY]: setting });
|
|
55
|
+
}
|
|
56
|
+
// Where the copies go, as an absolute directory path.
|
|
57
|
+
export function downloadsCopiesDir(downloadsDir) {
|
|
58
|
+
return joinPath(downloadsDir, DOWNLOADS_COPIES_SUBDIR);
|
|
59
|
+
}
|
|
60
|
+
// The collection the setting names, or — failing the id — whichever
|
|
61
|
+
// collection already sits at the copies directory. Null when neither
|
|
62
|
+
// exists yet.
|
|
63
|
+
export function findDownloadsCollection(setting, collections) {
|
|
64
|
+
if (!setting)
|
|
65
|
+
return null;
|
|
66
|
+
const byId = setting.collectionId
|
|
67
|
+
? collections.find((c) => c.id === setting.collectionId)
|
|
68
|
+
: undefined;
|
|
69
|
+
const wantedPath = dirKey(downloadsCopiesDir(setting.downloadsDir));
|
|
70
|
+
// A collection the id names but that has since moved out from under
|
|
71
|
+
// us — Chrome's Downloads location changed — is an ordinary directory
|
|
72
|
+
// collection now, not ours.
|
|
73
|
+
if (byId && dirKey(byId.path) === wantedPath)
|
|
74
|
+
return byId;
|
|
75
|
+
return collections.find((c) => dirKey(c.path) === wantedPath) ?? null;
|
|
76
|
+
}
|
|
77
|
+
// A directory path or `file://` URL as a comparable key: the bare
|
|
78
|
+
// path, with no trailing slash.
|
|
79
|
+
function dirKey(pathOrUrl) {
|
|
80
|
+
return canonicalCollectionInput(pathOrUrl).replace(/\/+$/, '');
|
|
81
|
+
}
|
|
82
|
+
// ---------- Names and paths ----------
|
|
83
|
+
// The relative path a copy of `script` gets by default: its own file
|
|
84
|
+
// name, made a `.user.js` if it isn't one. Nothing cleverer — a copy
|
|
85
|
+
// of a copy defaults to the same name too, and the dialog refuses
|
|
86
|
+
// that (`isCopyOfItself`) rather than inventing a name the user didn't
|
|
87
|
+
// ask for.
|
|
88
|
+
export function defaultCopyName(script) {
|
|
89
|
+
const base = scriptBasename(script.relativePath).replace(/[\\/:*?"<>|]/g, '_');
|
|
90
|
+
return base.endsWith('.user.js') ? base : `${base.replace(/\.js$/, '')}.user.js`;
|
|
91
|
+
}
|
|
92
|
+
// Two sentences, so both take a period — see docs/diagnostics.md.
|
|
93
|
+
export const COPY_OVER_ITSELF = 'The copy source and destination are the same. Choose another filename.';
|
|
94
|
+
// True when writing `fileName` under `copiesDir` would land on the very
|
|
95
|
+
// file `script` is loaded from — a copy standing in the copies
|
|
96
|
+
// directory, copied again under its own name.
|
|
97
|
+
export function isCopyOfItself(script, copiesDir, fileName) {
|
|
98
|
+
return locationKey(script.fileUrl) === locationKey(pathToFileUrl(joinPath(copiesDir, fileName.trim())));
|
|
99
|
+
}
|
|
100
|
+
export const COPY_NAME_REQUIRED = 'Filename is required';
|
|
101
|
+
export const COPY_NAME_NOT_USERSCRIPT = 'Filename must end with .user.js';
|
|
102
|
+
export const COPY_NAME_NOT_RELATIVE = 'Filename must be a relative path, with no leading slash or ..';
|
|
103
|
+
export const COPY_NAME_TOO_DEEP = `Filename can be at most ${MAX_SCAN_DEPTH} folders deep, or the collection will not find it`;
|
|
104
|
+
export const COPY_NAME_BAD_CHARS = 'Filename cannot contain \\ : * ? " < > |';
|
|
105
|
+
// Why `relativePath` can't name a copy, or null when it can. Mirrors
|
|
106
|
+
// what `chrome.downloads.download` refuses (absolute paths, `..`) and
|
|
107
|
+
// what the collection scan would never find (too deep).
|
|
108
|
+
export function validateCopyName(relativePath) {
|
|
109
|
+
const name = relativePath.trim();
|
|
110
|
+
if (!name)
|
|
111
|
+
return COPY_NAME_REQUIRED;
|
|
112
|
+
if (UNWRITABLE_CHARS.test(name))
|
|
113
|
+
return COPY_NAME_BAD_CHARS;
|
|
114
|
+
if (name.startsWith('/'))
|
|
115
|
+
return COPY_NAME_NOT_RELATIVE;
|
|
116
|
+
const segments = name.split('/');
|
|
117
|
+
// `.trim()` catches a blank segment too — `a/ /b.user.js` would make
|
|
118
|
+
// a directory named with a space.
|
|
119
|
+
if (segments.some((s) => s.trim() === '' || s === '.' || s === '..')) {
|
|
120
|
+
return COPY_NAME_NOT_RELATIVE;
|
|
121
|
+
}
|
|
122
|
+
if (!name.endsWith('.user.js') || segments[segments.length - 1] === '.user.js') {
|
|
123
|
+
return COPY_NAME_NOT_USERSCRIPT;
|
|
124
|
+
}
|
|
125
|
+
if (segments.length - 1 > MAX_SCAN_DEPTH)
|
|
126
|
+
return COPY_NAME_TOO_DEEP;
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
// The Downloads folder a saved file implies: the absolute path Chrome
|
|
130
|
+
// reported, minus the relative name we asked for. Null when the file
|
|
131
|
+
// went somewhere else — the user has Chrome's "Ask where to save each
|
|
132
|
+
// file" on and picked another folder.
|
|
133
|
+
export function downloadsDirFromSaved(absolutePath, relativeName) {
|
|
134
|
+
const suffix = `/${relativeName}`;
|
|
135
|
+
if (!absolutePath.endsWith(suffix))
|
|
136
|
+
return null;
|
|
137
|
+
const dir = absolutePath.slice(0, -suffix.length);
|
|
138
|
+
return dir || null;
|
|
139
|
+
}
|
|
140
|
+
// The source with `@version` / `@description` replaced, added, or
|
|
141
|
+
// removed. Line-for-line otherwise: every other line, the line endings,
|
|
142
|
+
// and the spacing between tag and value are kept as they were, so a
|
|
143
|
+
// diff against the original shows only the edit. A source with no
|
|
144
|
+
// metadata block is returned unchanged — there is nowhere to write.
|
|
145
|
+
export function rewriteHeaderTags(source, edits) {
|
|
146
|
+
const split = splitHeaderBody(source);
|
|
147
|
+
if (split.error)
|
|
148
|
+
return source;
|
|
149
|
+
const eol = split.headerText.includes('\r\n') ? '\r\n' : '\n';
|
|
150
|
+
const lines = split.headerText.split(/\r\n|\r|\n/);
|
|
151
|
+
for (const [tag, value] of [['version', edits.version], ['description', edits.description]]) {
|
|
152
|
+
if (value === undefined)
|
|
153
|
+
continue;
|
|
154
|
+
applyTagEdit(lines, tag, value.trim());
|
|
155
|
+
}
|
|
156
|
+
// Not `joinFullSource`: it restores the line break after the closing
|
|
157
|
+
// marker as a bare `\n`, which is fine for display but would leave a
|
|
158
|
+
// CRLF file with one LF line in the middle.
|
|
159
|
+
return split.leadingText + lines.join(eol) + eol + split.bodyText;
|
|
160
|
+
}
|
|
161
|
+
// Plain `@tag` only — a localized `@description:de` is a different tag
|
|
162
|
+
// and is left alone.
|
|
163
|
+
function tagLinePattern(tag) {
|
|
164
|
+
return new RegExp(`^([ \\t]*\\/\\/[ \\t]*@${tag})(?:([ \\t]+)(.*\\S))?[ \\t]*$`);
|
|
165
|
+
}
|
|
166
|
+
function applyTagEdit(lines, tag, value) {
|
|
167
|
+
const re = tagLinePattern(tag);
|
|
168
|
+
const at = lines.findIndex((l) => re.test(l));
|
|
169
|
+
if (at >= 0) {
|
|
170
|
+
if (!value) {
|
|
171
|
+
lines.splice(at, 1);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const m = re.exec(lines[at]);
|
|
175
|
+
lines[at] = `${m[1]}${m[2] ?? ' '}${value}`;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (!value)
|
|
179
|
+
return;
|
|
180
|
+
// New line, placed after `@name` (any locale) and aligned to it, or
|
|
181
|
+
// straight after the opening marker when there is no `@name`.
|
|
182
|
+
const nameRe = /^([ \t]*\/\/[ \t]*@)name(?::[\w-]+)?(?:([ \t]+)(.*\S))?[ \t]*$/;
|
|
183
|
+
const nameAt = lines.findIndex((l) => nameRe.test(l));
|
|
184
|
+
let prefix = '// @';
|
|
185
|
+
let valueColumn = -1;
|
|
186
|
+
if (nameAt >= 0) {
|
|
187
|
+
const m = nameRe.exec(lines[nameAt]);
|
|
188
|
+
prefix = m[1];
|
|
189
|
+
if (m[3] !== undefined)
|
|
190
|
+
valueColumn = lines[nameAt].length - m[3].length;
|
|
191
|
+
}
|
|
192
|
+
const head = `${prefix}${tag}`;
|
|
193
|
+
const pad = valueColumn > head.length ? ' '.repeat(valueColumn - head.length) : ' ';
|
|
194
|
+
lines.splice(nameAt >= 0 ? nameAt + 1 : 1, 0, `${head}${pad}${value}`);
|
|
195
|
+
}
|
|
196
|
+
// The dependency files a copy of `script` would carry: the bytes the
|
|
197
|
+
// row already holds, laid out as above. Deduped by target path, so two
|
|
198
|
+
// declarations of one URL write one file. Skipped: an absolute URL
|
|
199
|
+
// that isn't http(s) (a `data:` one has no file to write), and a
|
|
200
|
+
// relative ref that climbs out of the script's folder, which no path
|
|
201
|
+
// under the copy's folder can satisfy.
|
|
202
|
+
export function dependencyFiles(script) {
|
|
203
|
+
const out = new Map();
|
|
204
|
+
const add = (ref, url, body) => {
|
|
205
|
+
const relativePath = isRelativeRef(ref)
|
|
206
|
+
? writable(relativeRefPath(ref))
|
|
207
|
+
: isHttpUrl(url) ? mirrorPath(url) : null;
|
|
208
|
+
if (!relativePath || out.has(relativePath))
|
|
209
|
+
return;
|
|
210
|
+
out.set(relativePath, { url, relativePath, body, size: body.size });
|
|
211
|
+
};
|
|
212
|
+
for (const r of script.requires ?? []) {
|
|
213
|
+
add(r.ref, r.url, new Blob([r.source], { type: 'text/javascript' }));
|
|
214
|
+
}
|
|
215
|
+
for (const r of script.resources ?? []) {
|
|
216
|
+
add(r.ref, r.url, new Blob([base64ToBytes(r.bytesBase64)], { type: r.contentType || 'application/octet-stream' }));
|
|
217
|
+
}
|
|
218
|
+
return [...out.values()];
|
|
219
|
+
}
|
|
220
|
+
// A ref with no scheme and no leading `/`: one that resolves against the
|
|
221
|
+
// script's own folder.
|
|
222
|
+
export function isRelativeRef(ref) {
|
|
223
|
+
const trimmed = ref.trim().split('#')[0];
|
|
224
|
+
return trimmed !== '' && !/^[a-z][a-z0-9+.-]*:/i.test(trimmed) && !trimmed.startsWith('/');
|
|
225
|
+
}
|
|
226
|
+
// Where a relative ref lands under its script's folder, decoded to the
|
|
227
|
+
// on-disk spelling, so the same ref finds it beside a copy or an
|
|
228
|
+
// unzipped export. Null when it climbs out of that folder or names a
|
|
229
|
+
// directory. An encoded `/` (`..%2F`) counts as unusable rather than
|
|
230
|
+
// decoding into a path the ref never named.
|
|
231
|
+
export function relativeRefPath(ref) {
|
|
232
|
+
const root = '/sourcemonkey-ref-root/';
|
|
233
|
+
let resolved;
|
|
234
|
+
try {
|
|
235
|
+
resolved = new URL(ref.trim(), `file://${root}script.user.js`);
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
if (!resolved.pathname.startsWith(root))
|
|
241
|
+
return null;
|
|
242
|
+
const rest = resolved.pathname.slice(root.length);
|
|
243
|
+
if (!rest || rest.endsWith('/'))
|
|
244
|
+
return null;
|
|
245
|
+
let segments;
|
|
246
|
+
try {
|
|
247
|
+
segments = rest.split('/').map((seg) => decodeURIComponent(seg));
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
return segments.some((seg) => seg.includes('/')) ? null : segments.join('/');
|
|
253
|
+
}
|
|
254
|
+
// `<host>/<path>` of a URL as an on-disk relative path, via the same
|
|
255
|
+
// candidate builder the override lookup uses so the two can't drift.
|
|
256
|
+
// Null when the URL yields no candidate (no path, or a path that names
|
|
257
|
+
// a directory).
|
|
258
|
+
//
|
|
259
|
+
// The longest candidate `chrome.downloads` can actually write. It
|
|
260
|
+
// refuses a path with characters illegal on any platform — `:` above
|
|
261
|
+
// all, which a `host:port` mirror directory would carry. The lookup
|
|
262
|
+
// tries every shorter suffix too, so dropping to `lib/util.js` beside
|
|
263
|
+
// the script still resolves; a basename that itself can't be written
|
|
264
|
+
// yields null and the dependency is skipped.
|
|
265
|
+
export function mirrorPath(url) {
|
|
266
|
+
// A stand-in root no real file sits under: the candidate builder
|
|
267
|
+
// offers nothing for a URL already inside its root, which `file:///`
|
|
268
|
+
// would be for every `file://` URL.
|
|
269
|
+
const root = 'file:///sourcemonkey-mirror-root/';
|
|
270
|
+
for (const candidate of localFileCandidates(url, [root])) {
|
|
271
|
+
if (!candidate.startsWith(root))
|
|
272
|
+
continue;
|
|
273
|
+
let decoded;
|
|
274
|
+
try {
|
|
275
|
+
decoded = candidate.slice(root.length).split('/').map((seg) => decodeURIComponent(seg)).join('/');
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (!UNWRITABLE_CHARS.test(decoded))
|
|
281
|
+
return decoded;
|
|
282
|
+
}
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
function writable(path) {
|
|
286
|
+
return path !== null && !UNWRITABLE_CHARS.test(path) ? path : null;
|
|
287
|
+
}
|
|
288
|
+
// What `chrome.downloads.download` rejects in a `filename` — the
|
|
289
|
+
// Windows-illegal set, enforced everywhere. Same set `validateCopyName`
|
|
290
|
+
// refuses for the script's own name.
|
|
291
|
+
const UNWRITABLE_CHARS = /[\\:*?"<>|]/;
|
|
292
|
+
// Every file the copy writes, the script first. Dependencies sit under
|
|
293
|
+
// the script's own folder.
|
|
294
|
+
export function planFiles(script, plan) {
|
|
295
|
+
const source = joinFullSource(script.leadingText ?? '', script.headerText, script.bodyText);
|
|
296
|
+
const text = rewriteHeaderTags(source, { version: plan.version, description: plan.description });
|
|
297
|
+
const files = [
|
|
298
|
+
{ relativePath: plan.fileName, body: new Blob([text], { type: 'text/javascript' }) },
|
|
299
|
+
];
|
|
300
|
+
if (plan.includeDependencies) {
|
|
301
|
+
const dir = plan.fileName.includes('/') ? plan.fileName.slice(0, plan.fileName.lastIndexOf('/')) : '';
|
|
302
|
+
for (const dep of dependencyFiles(script)) {
|
|
303
|
+
files.push({ relativePath: dir ? `${dir}/${dep.relativePath}` : dep.relativePath, body: dep.body });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return files;
|
|
307
|
+
}
|
|
308
|
+
// Which of `files` already exist under `copiesDir`, read through the
|
|
309
|
+
// same directory listing a refresh uses. A listing that can't be read
|
|
310
|
+
// (the folder doesn't exist yet, say) counts as empty — the write will
|
|
311
|
+
// create it.
|
|
312
|
+
export async function checkExistingFiles(copiesDir, files) {
|
|
313
|
+
const cache = new Map();
|
|
314
|
+
const existing = [];
|
|
315
|
+
for (const f of files) {
|
|
316
|
+
const abs = joinPath(copiesDir, f.relativePath);
|
|
317
|
+
const slash = abs.lastIndexOf('/');
|
|
318
|
+
let entry = null;
|
|
319
|
+
try {
|
|
320
|
+
entry = await statFromListing(abs.slice(0, slash), abs.slice(slash + 1), cache);
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
// No listing, no file.
|
|
324
|
+
}
|
|
325
|
+
if (!entry || entry.isDir)
|
|
326
|
+
continue;
|
|
327
|
+
existing.push(f.relativePath);
|
|
328
|
+
}
|
|
329
|
+
return { existing };
|
|
330
|
+
}
|
|
331
|
+
// ---------- Writing ----------
|
|
332
|
+
export const COPY_CANCELLED = 'Saving the copy was cancelled';
|
|
333
|
+
// How long to wait for one download to finish. A blob of a few hundred
|
|
334
|
+
// KB completes in milliseconds; the budget only matters when Chrome is
|
|
335
|
+
// holding the file for a Save As box the user has left open.
|
|
336
|
+
const DOWNLOAD_TIMEOUT_MS = 10 * 60_000;
|
|
337
|
+
export async function writeUnderDownloads(files, overwrite) {
|
|
338
|
+
const saved = [];
|
|
339
|
+
for (const f of files) {
|
|
340
|
+
saved.push(await downloadOne(f.relativePath, f.body, overwrite));
|
|
341
|
+
}
|
|
342
|
+
return saved;
|
|
343
|
+
}
|
|
344
|
+
// Delete files this copy wrote, after it turned out not to be a copy.
|
|
345
|
+
// Best effort and never thrown from: the caller is already reporting a
|
|
346
|
+
// failure, and a file we couldn't remove is a smaller problem than
|
|
347
|
+
// replacing that report with a second one.
|
|
348
|
+
//
|
|
349
|
+
// Why bother: a `.user.js` left behind in the copies directory is not
|
|
350
|
+
// inert. The next refresh of that collection scans it in as a new
|
|
351
|
+
// script — and a new row lands ENABLED — so an abandoned copy would
|
|
352
|
+
// start running a script the user never agreed to install.
|
|
353
|
+
async function discardWritten(written) {
|
|
354
|
+
for (const w of written) {
|
|
355
|
+
try {
|
|
356
|
+
await chrome.downloads.removeFile(w.id);
|
|
357
|
+
}
|
|
358
|
+
catch (err) {
|
|
359
|
+
dlog(`[SourceMonkey] could not remove ${w.path} after a failed copy: ${describeError(err)}`);
|
|
360
|
+
}
|
|
361
|
+
// The download-history entry too, so the failed copy doesn't sit in
|
|
362
|
+
// the user's downloads list pointing at a file that is gone.
|
|
363
|
+
try {
|
|
364
|
+
await chrome.downloads.erase({ id: w.id });
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
// Nothing to do about it, and nothing to tell the user.
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
async function downloadOne(relativePath, body, overwrite) {
|
|
372
|
+
const url = URL.createObjectURL(body);
|
|
373
|
+
try {
|
|
374
|
+
let id;
|
|
375
|
+
try {
|
|
376
|
+
id = await chrome.downloads.download({
|
|
377
|
+
url,
|
|
378
|
+
filename: relativePath,
|
|
379
|
+
saveAs: false,
|
|
380
|
+
conflictAction: overwrite ? 'overwrite' : 'uniquify',
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
catch (err) {
|
|
384
|
+
// Chrome's own wording ("Invalid filename") names neither the
|
|
385
|
+
// file nor the step.
|
|
386
|
+
throw new Error(`Saving ${relativePath} was refused by Chrome: ${describeChromeError(err)}`);
|
|
387
|
+
}
|
|
388
|
+
const deadline = Date.now() + DOWNLOAD_TIMEOUT_MS;
|
|
389
|
+
let accepted = false;
|
|
390
|
+
while (Date.now() < deadline) {
|
|
391
|
+
const [item] = await chrome.downloads.search({ id });
|
|
392
|
+
if (item) {
|
|
393
|
+
if (item.state === 'complete')
|
|
394
|
+
return { id, path: item.filename };
|
|
395
|
+
if (item.state === 'interrupted') {
|
|
396
|
+
if (item.error === 'USER_CANCELED')
|
|
397
|
+
throw new Error(COPY_CANCELLED);
|
|
398
|
+
throw new Error(`Saving ${relativePath} failed: ${item.error ?? 'download interrupted'}`);
|
|
399
|
+
}
|
|
400
|
+
// A file type Chrome wants confirmed before it lands. Not
|
|
401
|
+
// expected for `.user.js` (verified), but the prompt is the
|
|
402
|
+
// right answer if a Chrome policy ever flags it — once.
|
|
403
|
+
if (item.danger !== 'safe' && item.danger !== 'accepted' && !accepted) {
|
|
404
|
+
accepted = true;
|
|
405
|
+
await chrome.downloads.acceptDanger(id);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
409
|
+
}
|
|
410
|
+
throw new Error(`Saving ${relativePath} timed out`);
|
|
411
|
+
}
|
|
412
|
+
finally {
|
|
413
|
+
// The download has read the blob by the time `search` reports it
|
|
414
|
+
// complete or interrupted, and a URL that outlives its page dies
|
|
415
|
+
// with it either way.
|
|
416
|
+
URL.revokeObjectURL(url);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
export const COPY_NOT_FOUND_AFTER_REFRESH = 'Internal error: the copy was written but the collection did not find it';
|
|
420
|
+
// Write the copy (and any dependencies), then make sure the Downloads
|
|
421
|
+
// collection exists and has scanned it in. Resolves with the new row.
|
|
422
|
+
//
|
|
423
|
+
// The first copy ever is the one that learns where Downloads is, so it
|
|
424
|
+
// has to write before it can create the collection; every later copy
|
|
425
|
+
// finds the setting in place. One order serves both.
|
|
426
|
+
export async function makeLocalCopy(request) {
|
|
427
|
+
const { script, plan } = request;
|
|
428
|
+
const files = planFiles(script, plan).map((f) => ({
|
|
429
|
+
...f,
|
|
430
|
+
downloadPath: `${DOWNLOADS_COPIES_SUBDIR}/${f.relativePath}`,
|
|
431
|
+
}));
|
|
432
|
+
// Only for telling a rename apart from a redirect below; the
|
|
433
|
+
// authoritative answer is the path the write itself reports.
|
|
434
|
+
const priorDir = (await readDownloadsCopiesSetting())?.downloadsDir ?? null;
|
|
435
|
+
const written = [];
|
|
436
|
+
let downloadsDir = '';
|
|
437
|
+
try {
|
|
438
|
+
for (const f of files) {
|
|
439
|
+
const one = await downloadOneChecked(f, request.overwrite);
|
|
440
|
+
written.push(one);
|
|
441
|
+
// Every file has to have landed where it was asked to: the script
|
|
442
|
+
// because that is what gets registered, the dependencies because
|
|
443
|
+
// a renamed one is a file the override lookup will never find.
|
|
444
|
+
// The first file is also what teaches us where Downloads is.
|
|
445
|
+
const dir = downloadsDirFromSaved(one.path, f.downloadPath);
|
|
446
|
+
if (!dir || (downloadsDir && dir !== downloadsDir)) {
|
|
447
|
+
throw new Error(savedElsewhere(one.path, f.downloadPath, downloadsDir ? downloadsCopiesDir(downloadsDir)
|
|
448
|
+
: priorDir && downloadsCopiesDir(priorDir)));
|
|
449
|
+
}
|
|
450
|
+
downloadsDir = dir;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
catch (err) {
|
|
454
|
+
// Nothing is registered, so anything already on disk is a file the
|
|
455
|
+
// user never asked to keep — and a `.user.js` among them would be
|
|
456
|
+
// scanned in as a script on the next refresh.
|
|
457
|
+
await discardWritten(written);
|
|
458
|
+
throw err;
|
|
459
|
+
}
|
|
460
|
+
// The files are on disk; the worker, which alone writes storage,
|
|
461
|
+
// makes them a script (docs/worker-writes.md).
|
|
462
|
+
const registered = await callWorker('registerLocalCopy', downloadsDir, plan.fileName);
|
|
463
|
+
return { ...registered, path: written[0].path };
|
|
464
|
+
}
|
|
465
|
+
// Worker side of a local copy, once its files are on disk in the
|
|
466
|
+
// copies directory under `downloadsDir`: the collection that holds
|
|
467
|
+
// them, refreshed so it lists the copy, and the copy turned on.
|
|
468
|
+
export async function registerLocalCopy(downloadsDir, fileName) {
|
|
469
|
+
const collection = await ensureDownloadsCollection(downloadsDir);
|
|
470
|
+
const refreshed = await refreshCollection(collection.id);
|
|
471
|
+
const row = refreshed?.scripts.find((s) => s.relativePath === fileName);
|
|
472
|
+
if (!row)
|
|
473
|
+
throw new Error(COPY_NOT_FOUND_AFTER_REFRESH);
|
|
474
|
+
// A new copy is an install, so its Log starts with one. A row that was
|
|
475
|
+
// already listed (a replaced copy, or one a new collection's own add
|
|
476
|
+
// scanned in and logged) has its Log already.
|
|
477
|
+
if (!collection.scriptIds.includes(row.id))
|
|
478
|
+
await emitInitialLoadLog(row, true);
|
|
479
|
+
// Without its bodies: the page only needs which row it is.
|
|
480
|
+
return { script: withoutBodies(await enableCopy(row)), collection: refreshed?.collection ?? collection };
|
|
481
|
+
}
|
|
482
|
+
// One file, written and reported. Split out only so `makeLocalCopy`'s
|
|
483
|
+
// loop reads as the checks it performs.
|
|
484
|
+
async function downloadOneChecked(file, overwrite) {
|
|
485
|
+
return await downloadOne(file.downloadPath, file.body, overwrite);
|
|
486
|
+
}
|
|
487
|
+
// A copy is meant to be the version that runs, and the caller's
|
|
488
|
+
// exclusive-enable sweep only acts on a row that is already running.
|
|
489
|
+
// A REPLACED file inherits the row it replaced — including a prior
|
|
490
|
+
// `enabled: false`, which `rebuildCollection` deliberately preserves
|
|
491
|
+
// across a refresh — so without this, replacing a copy the user had
|
|
492
|
+
// switched off would land them on a page with nothing running and no
|
|
493
|
+
// explanation. Failing to write it is not worth blocking the copy over:
|
|
494
|
+
// the row exists either way, and the page shows its real enable state.
|
|
495
|
+
async function enableCopy(row) {
|
|
496
|
+
if (row.enabled)
|
|
497
|
+
return row;
|
|
498
|
+
try {
|
|
499
|
+
await setScriptsEnabled([{ id: row.id, enabled: true }]);
|
|
500
|
+
return { ...row, enabled: true };
|
|
501
|
+
}
|
|
502
|
+
catch (err) {
|
|
503
|
+
dlog(`[SourceMonkey] could not enable the new copy: ${describeError(err)}`);
|
|
504
|
+
return row;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
function describeChromeError(err) {
|
|
508
|
+
return err instanceof Error ? err.message : String(err);
|
|
509
|
+
}
|
|
510
|
+
// A file that didn't land where it was asked to. Two different things
|
|
511
|
+
// happened, and the fix differs, so they are two messages rather than
|
|
512
|
+
// one that guesses — see docs/diagnostics.md#wording.
|
|
513
|
+
//
|
|
514
|
+
// `copiesDir` is what we can compare against: null on the very first
|
|
515
|
+
// copy, where the Downloads folder is still unknown and "a different
|
|
516
|
+
// folder" is the only thing that can be said.
|
|
517
|
+
function savedElsewhere(path, asked, copiesDir) {
|
|
518
|
+
const dir = path.slice(0, path.lastIndexOf('/'));
|
|
519
|
+
if (copiesDir && dir === copiesDir) {
|
|
520
|
+
// Chrome renamed around a file that appeared between the dialog's
|
|
521
|
+
// check and the write. The copy is on disk under a name nobody
|
|
522
|
+
// asked for, so it is not registered.
|
|
523
|
+
return `Chrome saved the copy as ${baseName(path)} instead of ${baseName(asked)}, `
|
|
524
|
+
+ 'so it was not added as a script. Delete that file, or copy again to replace it.';
|
|
525
|
+
}
|
|
526
|
+
return `The copy was saved to ${path}, outside the collection directory, `
|
|
527
|
+
+ 'so it was not added as a script';
|
|
528
|
+
}
|
|
529
|
+
// The Downloads collection for `downloadsDir`: the one the setting
|
|
530
|
+
// names, else one already at that directory (adopted), else a new one.
|
|
531
|
+
// Records what it found so the next copy needs no lookup by path.
|
|
532
|
+
export async function ensureDownloadsCollection(downloadsDir) {
|
|
533
|
+
const prior = await readDownloadsCopiesSetting();
|
|
534
|
+
const setting = { downloadsDir, collectionId: prior?.collectionId ?? null };
|
|
535
|
+
const collections = await getCollections();
|
|
536
|
+
let collection = findDownloadsCollection(setting, collections);
|
|
537
|
+
if (!collection) {
|
|
538
|
+
const created = await createCollection({
|
|
539
|
+
kind: 'location',
|
|
540
|
+
path: downloadsCopiesDir(downloadsDir),
|
|
541
|
+
// The seeded name, unless something already wears it — the
|
|
542
|
+
// collection at the previous Downloads location, say. Then the
|
|
543
|
+
// path-derived name will do; nothing looks this collection up by
|
|
544
|
+
// its name.
|
|
545
|
+
name: collectionNameInUse(DOWNLOADS_COPIES_NAME, collections) ? undefined : DOWNLOADS_COPIES_NAME,
|
|
546
|
+
});
|
|
547
|
+
// `createCollection` answers null for a path that is already a
|
|
548
|
+
// collection — which `findDownloadsCollection` just said there
|
|
549
|
+
// wasn't, so another tab beat us to it. Take theirs.
|
|
550
|
+
collection = created?.kind === 'newCollection'
|
|
551
|
+
? created.collection
|
|
552
|
+
: findDownloadsCollection(setting, await getCollections());
|
|
553
|
+
if (!collection)
|
|
554
|
+
throw new Error('Internal error: could not create the Downloads collection');
|
|
555
|
+
}
|
|
556
|
+
if (prior?.downloadsDir !== downloadsDir || prior.collectionId !== collection.id) {
|
|
557
|
+
await writeDownloadsCopiesSetting({ downloadsDir, collectionId: collection.id });
|
|
558
|
+
}
|
|
559
|
+
return collection;
|
|
560
|
+
}
|