@augmentd-labs/canvas-edge 0.0.0-stage → 0.4.1

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.
Files changed (44) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +112 -0
  3. package/README.md +76 -2
  4. package/bin/canvas-edge +2 -0
  5. package/bin/healthcheck.mjs +9 -0
  6. package/node_modules/canvas-stored/LICENSE +661 -0
  7. package/node_modules/canvas-stored/NOTICE +85 -0
  8. package/node_modules/canvas-stored/README.md +472 -0
  9. package/node_modules/canvas-stored/package.json +26 -0
  10. package/node_modules/canvas-stored/src/backends/BackendManager.js +60 -0
  11. package/node_modules/canvas-stored/src/backends/StorageBackend.js +72 -0
  12. package/node_modules/canvas-stored/src/backends/cacache/index.js +130 -0
  13. package/node_modules/canvas-stored/src/backends/canvas/index.js +722 -0
  14. package/node_modules/canvas-stored/src/backends/file/index.js +658 -0
  15. package/node_modules/canvas-stored/src/backends/gdrive/index.js +924 -0
  16. package/node_modules/canvas-stored/src/backends/http/index.js +47 -0
  17. package/node_modules/canvas-stored/src/backends/s3/index.js +25 -0
  18. package/node_modules/canvas-stored/src/cache/index.js +65 -0
  19. package/node_modules/canvas-stored/src/extractors/audio.js +53 -0
  20. package/node_modules/canvas-stored/src/extractors/image.js +90 -0
  21. package/node_modules/canvas-stored/src/extractors/index.js +41 -0
  22. package/node_modules/canvas-stored/src/extractors/text.js +65 -0
  23. package/node_modules/canvas-stored/src/index/index.js +360 -0
  24. package/node_modules/canvas-stored/src/index.js +2058 -0
  25. package/node_modules/canvas-stored/src/sync/JobQueue.js +0 -0
  26. package/node_modules/canvas-stored/src/sync/Ledger.js +138 -0
  27. package/node_modules/canvas-stored/src/sync/Mirror.js +995 -0
  28. package/node_modules/canvas-stored/src/sync/SyncQueue.js +111 -0
  29. package/node_modules/canvas-stored/src/sync/keys.js +140 -0
  30. package/node_modules/canvas-stored/src/sync/worker.js +52 -0
  31. package/node_modules/canvas-stored/src/utils/checksum.js +32 -0
  32. package/node_modules/canvas-stored/src/utils/common.js +37 -0
  33. package/node_modules/canvas-stored/src/utils/mime.js +107 -0
  34. package/node_modules/canvas-stored/src/utils/mount.js +99 -0
  35. package/package.json +49 -4
  36. package/src/EdgeClient.js +129 -0
  37. package/src/control.js +48 -0
  38. package/src/daemon.js +88 -0
  39. package/src/env.js +182 -0
  40. package/src/fuse-runtime.js +173 -0
  41. package/src/index.js +26 -0
  42. package/src/mirror-runtime.js +232 -0
  43. package/src/remote-config.js +69 -0
  44. package/src/runtime.js +61 -0
@@ -0,0 +1,72 @@
1
+ import { EventEmitter } from 'events';
2
+
3
+ /**
4
+ * Base class for storage backends.
5
+ * All backends must implement: put, get, delete, stat, list
6
+ * Optional: watch, scan, stop
7
+ */
8
+ export default class StorageBackend extends EventEmitter {
9
+ constructor(name, config = {}) {
10
+ super();
11
+ this.name = name;
12
+ this.type = 'base';
13
+ this.config = config;
14
+ }
15
+
16
+ /**
17
+ * Capability flags consumed by the Destroy/lifecycle logic.
18
+ * Default is fully read-write; read-only backends (e.g. HTTP) override.
19
+ * `delete:false` means a location on this backend can only be dropped as a
20
+ * reference — its bytes cannot be removed by us.
21
+ * `canEnumerate:true` means the backend implements the async-generator
22
+ * `list()` (and usually `scan()`) so callers can mirror its whole tree;
23
+ * drivers without real enumeration (http, s3 skeleton) leave it false.
24
+ * `remote` mirrors the getter below — carried here so a single capabilities
25
+ * snapshot is enough for consumers deciding how expensive an op will be.
26
+ */
27
+ get capabilities() { return { read: true, write: true, delete: true, canEnumerate: false, remote: this.remote }; }
28
+ get canDelete() { return this.capabilities.delete !== false; }
29
+ get canWrite() { return this.capabilities.write !== false; }
30
+ get canRead() { return this.capabilities.read !== false; }
31
+
32
+ /**
33
+ * Is the data behind this backend reached over the network?
34
+ *
35
+ * Distinct from `type` on purpose: `type: 'local'|'remote'` decides the
36
+ * WRITE path (synchronous put vs cache + SyncQueue), and an NFS/CIFS mount
37
+ * is still a synchronous POSIX write target — `type:'local'`, `remote:true`.
38
+ * This flag is about latency and trust: no reliable inotify, expensive
39
+ * full-file hashing, and a root that can disappear wholesale.
40
+ */
41
+ get remote() { return this.config.remote ?? (this.type === 'remote'); }
42
+
43
+ /** Transport serving the bytes ('cifs', 'nfs4', 'fuse.sshfs', 's3', …) or null. */
44
+ get transport() { return this.config.transport ?? null; }
45
+
46
+ // The real, protocol-native URL for `key` (e.g. https://…, s3://…, smb://…,
47
+ // file://…), used for provenance/UI. `stored://<backend>/<key>` remains the
48
+ // canonical fetch form. Returns null when the backend has no meaningful one.
49
+ nativeUrl(key) { return null; }
50
+
51
+ // Required methods - must be implemented by subclasses
52
+ async put(key, data) { throw new Error('Not implemented'); }
53
+ // Place an already-written file at `key` (streaming put commit). Local
54
+ // backends implement this; remote backends are fed via the cache + SyncQueue.
55
+ async commit(key, srcPath) { throw new Error('Not implemented'); }
56
+ async get(key, options = {}) { throw new Error('Not implemented'); }
57
+ async delete(key) { throw new Error('Not implemented'); }
58
+ async stat(key) { throw new Error('Not implemented'); }
59
+ async *list(options = {}) { throw new Error('Not implemented'); }
60
+
61
+ // Optional methods
62
+ async watch() { return false; }
63
+ async scan() { return []; }
64
+ async stop() { }
65
+
66
+ // Optional container (directory/folder) mutation — only backends with a real
67
+ // hierarchical namespace (the file driver) implement these. Advertised via
68
+ // the Workspace `mutableContainers` capability; callers must gate on it.
69
+ async createContainer(key) { throw new Error('Container ops not supported by this backend'); }
70
+ async deleteContainer(key) { throw new Error('Container ops not supported by this backend'); }
71
+ async renameContainer(fromKey, toKey) { throw new Error('Container ops not supported by this backend'); }
72
+ }
@@ -0,0 +1,130 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import { createReadStream } from 'fs';
4
+ import { pipeline } from 'stream/promises';
5
+ import cacache from 'cacache';
6
+ import contentPath from 'cacache/lib/content/path.js';
7
+ import Debug from 'debug';
8
+ import StorageBackend from '../StorageBackend.js';
9
+
10
+ const debug = Debug('stored:backend:cacache');
11
+
12
+ // memoize:false — this store fronts blobs up to ~20GB across 1M+ entries; never
13
+ // let cacache's in-memory LRU hold content. All bytes flow through streams.
14
+ const CACHE_OPTS = { memoize: false };
15
+
16
+ /**
17
+ * cacache storage backend — a content-addressable local blob store.
18
+ *
19
+ * Same key/value CRUD surface as FileBackend, but bytes live in a cacache
20
+ * content store (sha-keyed, deduped, integrity-checked) instead of a plain
21
+ * directory tree. Keys are arbitrary `stored://<backend>/<key>` keys mapped to
22
+ * content by cacache's index. `type = 'local'` so the Stored ingest path writes
23
+ * bytes immediately (no SyncQueue).
24
+ *
25
+ * There is no meaningful protocol-native URL (the store is internal), so
26
+ * `nativeUrl` is null and `stored://<backend>/<key>` is the only address.
27
+ *
28
+ * Not watched/scanned: it is a managed write target, not an external source —
29
+ * `watch`/`scan` inherit the no-op base behaviour.
30
+ *
31
+ * `config`: { root } — the cacache store directory ("the data route").
32
+ */
33
+ export default class CacacheBackend extends StorageBackend {
34
+ #root;
35
+ #algorithms;
36
+
37
+ constructor(name, config = {}) {
38
+ super(name, config);
39
+ if (!config.root) throw new Error('CacacheBackend requires root path');
40
+ this.#root = path.resolve(config.root);
41
+ this.#algorithms = config.algorithms || ['sha256'];
42
+ this.type = 'local';
43
+ fs.ensureDirSync(this.#root);
44
+ debug(`CacacheBackend "${name}" initialized at ${this.#root}`);
45
+ }
46
+
47
+ get root() { return this.#root; }
48
+ // list() streams the cacache index, so full-key enumeration works even
49
+ // though the store is opaque byte-wise.
50
+ get capabilities() { return { ...super.capabilities, canEnumerate: true }; }
51
+ // Staging dir for the Stored streaming-ingest hash pass (shares a filesystem
52
+ // with the store so the temp file is cheap to read back on commit).
53
+ get tempDir() { return path.join(this.#root, 'tmp'); }
54
+
55
+ // Internal blob store: no protocol-native URL to surface.
56
+ nativeUrl(key) { return null; }
57
+
58
+ // ─────────────────────────────────────────────────────────────────────────
59
+ // CRUD Operations
60
+ // ─────────────────────────────────────────────────────────────────────────
61
+
62
+ async put(key, data) {
63
+ await cacache.put(this.#root, key, data, { algorithms: this.#algorithms, ...CACHE_OPTS });
64
+ const size = Buffer.isBuffer(data) ? data.length : Buffer.byteLength(data);
65
+ debug(`PUT ${key} (${size} bytes)`);
66
+ return { key, size };
67
+ }
68
+
69
+ // Commit an already-written file (the Stored streaming-ingest temp) at `key`
70
+ // by streaming it into the content store.
71
+ async commit(key, srcPath) {
72
+ await pipeline(createReadStream(srcPath), cacache.put.stream(this.#root, key, { algorithms: this.#algorithms, ...CACHE_OPTS }));
73
+ const info = await cacache.get.info(this.#root, key);
74
+ const size = info?.size ?? 0;
75
+ debug(`COMMIT ${key} (${size} bytes)`);
76
+ return { key, size };
77
+ }
78
+
79
+ // Ranged read for HTTP Range/streaming: cacache has no ranged API, so read a
80
+ // byte window straight off the content-addressed blob file. `end` is
81
+ // inclusive (matches HTTP Range and fs.createReadStream). Returns null on a
82
+ // cache miss so the caller can fall back to a full read.
83
+ async getRange(key, { start, end }) {
84
+ const info = await cacache.get.info(this.#root, key);
85
+ if (!info) return null;
86
+ return createReadStream(contentPath(this.#root, info.integrity), { start, end });
87
+ }
88
+
89
+ async get(key, options = {}) {
90
+ if (options.stream) {
91
+ // get.stream emits ENOENT on miss; surface null instead of throwing.
92
+ const exists = await cacache.get.info(this.#root, key);
93
+ return exists ? cacache.get.stream(this.#root, key, CACHE_OPTS) : null;
94
+ }
95
+ try {
96
+ const { data } = await cacache.get(this.#root, key, CACHE_OPTS);
97
+ return data;
98
+ } catch (err) {
99
+ if (err.code === 'ENOENT') return null;
100
+ throw err;
101
+ }
102
+ }
103
+
104
+ async delete(key) {
105
+ const info = await cacache.get.info(this.#root, key);
106
+ if (!info) return false;
107
+ await cacache.rm.entry(this.#root, key, { removeFully: true });
108
+ debug(`DELETE ${key}`);
109
+ return true;
110
+ }
111
+
112
+ async stat(key) {
113
+ const info = await cacache.get.info(this.#root, key);
114
+ if (!info) return null;
115
+ return { key, size: info.size, modified: info.time, created: info.time };
116
+ }
117
+
118
+ async *list(options = {}) {
119
+ const { prefix = '' } = options;
120
+ // Stream the index so a 1M+ entry store is walked without ever building
121
+ // the full key→info map in memory.
122
+ for await (const info of cacache.ls.stream(this.#root)) {
123
+ if (prefix && !info.key.startsWith(prefix)) continue;
124
+ yield { key: info.key, size: info.size, modified: info.time, created: info.time };
125
+ }
126
+ }
127
+
128
+ // Drop the whole content store (index + content). Used by Destroy/reset paths.
129
+ async clear() { return cacache.rm.all(this.#root); }
130
+ }