@axium/storage 0.7.9 → 0.8.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.
Files changed (44) hide show
  1. package/dist/{client.d.ts → client/api.d.ts} +1 -1
  2. package/dist/{client.js → client/api.js} +12 -4
  3. package/dist/client/cli.d.ts +1 -0
  4. package/dist/client/cli.js +124 -0
  5. package/dist/client/config.d.ts +15 -0
  6. package/dist/client/config.js +21 -0
  7. package/dist/client/hooks.d.ts +1 -0
  8. package/dist/client/hooks.js +6 -0
  9. package/dist/client/index.d.ts +1 -0
  10. package/dist/client/index.js +1 -0
  11. package/dist/client/paths.d.ts +2 -0
  12. package/dist/client/paths.js +16 -0
  13. package/dist/client/sync.d.ts +55 -0
  14. package/dist/client/sync.js +205 -0
  15. package/dist/common.d.ts +60 -0
  16. package/dist/common.js +20 -0
  17. package/dist/polyfills.d.ts +1 -1
  18. package/dist/polyfills.js +3 -3
  19. package/dist/server/api.d.ts +1 -0
  20. package/dist/server/api.js +187 -0
  21. package/dist/server/batch.d.ts +1 -0
  22. package/dist/server/batch.js +147 -0
  23. package/dist/server/cli.d.ts +1 -0
  24. package/dist/server/cli.js +14 -0
  25. package/dist/{server.d.ts → server/config.d.ts} +12 -52
  26. package/dist/server/config.js +48 -0
  27. package/dist/server/db.d.ts +47 -0
  28. package/dist/server/db.js +77 -0
  29. package/dist/server/hooks.d.ts +9 -0
  30. package/dist/{plugin.js → server/hooks.js} +12 -14
  31. package/dist/server/index.d.ts +5 -0
  32. package/dist/server/index.js +5 -0
  33. package/dist/server/raw.d.ts +1 -0
  34. package/dist/server/raw.js +163 -0
  35. package/lib/List.svelte +2 -1
  36. package/lib/Usage.svelte +4 -2
  37. package/package.json +27 -16
  38. package/routes/files/[id]/+page.svelte +9 -5
  39. package/routes/files/shared/+page.svelte +2 -2
  40. package/routes/files/trash/+page.svelte +2 -2
  41. package/routes/files/usage/+page.svelte +5 -6
  42. package/dist/plugin.d.ts +0 -67
  43. package/dist/server.js +0 -412
  44. package/styles/list.css +0 -50
@@ -1,4 +1,4 @@
1
- import type { StorageItemMetadata, StorageItemUpdate, UserStorage, UserStorageInfo } from './common.js';
1
+ import type { StorageItemMetadata, StorageItemUpdate, UserStorage, UserStorageInfo } from '../common.js';
2
2
  export declare function parseItem(result: StorageItemMetadata): StorageItemMetadata;
3
3
  export interface UploadOptions {
4
4
  parentId?: string;
@@ -1,4 +1,4 @@
1
- import { fetchAPI, token } from '@axium/client/requests';
1
+ import { fetchAPI, prefix, token } from '@axium/client/requests';
2
2
  async function _upload(method, url, data, extraHeaders = {}) {
3
3
  const init = {
4
4
  method,
@@ -30,16 +30,24 @@ export function parseItem(result) {
30
30
  result.trashedAt = new Date(result.trashedAt);
31
31
  return result;
32
32
  }
33
+ function rawStorage(fileId) {
34
+ const raw = '/raw/storage' + (fileId ? '/' + fileId : '');
35
+ if (prefix[0] == '/')
36
+ return raw;
37
+ const url = new URL(prefix);
38
+ url.pathname = raw;
39
+ return url;
40
+ }
33
41
  export async function uploadItem(file, opt = {}) {
34
42
  const headers = {};
35
43
  if (opt.parentId)
36
44
  headers['x-parent'] = opt.parentId;
37
45
  if (opt.name)
38
46
  headers['x-name'] = opt.name;
39
- return parseItem(await _upload('PUT', '/raw/storage', file, headers));
47
+ return parseItem(await _upload('PUT', rawStorage(), file, headers));
40
48
  }
41
49
  export async function updateItem(fileId, data) {
42
- return parseItem(await _upload('POST', '/raw/storage/' + fileId, data));
50
+ return parseItem(await _upload('POST', rawStorage(fileId), data));
43
51
  }
44
52
  export async function getItemMetadata(fileId) {
45
53
  const result = await fetchAPI('GET', 'storage/item/:id', undefined, fileId);
@@ -55,7 +63,7 @@ export async function getDirectoryMetadata(parentId) {
55
63
  return result;
56
64
  }
57
65
  export async function downloadItem(fileId) {
58
- const response = await fetch('/raw/storage/' + fileId, {
66
+ const response = await fetch(rawStorage(fileId), {
59
67
  headers: token ? { Authorization: 'Bearer ' + token } : {},
60
68
  });
61
69
  if (!response.ok)
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,124 @@
1
+ import { configDir, session } from '@axium/client/cli/config';
2
+ import { formatBytes } from '@axium/core/format';
3
+ import * as io from '@axium/core/node/io';
4
+ import { Option, program } from 'commander';
5
+ import { statSync, unlinkSync } from 'node:fs';
6
+ import { stat } from 'node:fs/promises';
7
+ import { join, resolve } from 'node:path';
8
+ import { styleText } from 'node:util';
9
+ import { getUserStorage, getUserStorageInfo, getUserStorageRoot } from './api.js';
10
+ import { config, saveConfig } from './config.js';
11
+ import { walkItems } from './paths.js';
12
+ import { computeDelta, doSync, fetchSyncItems } from './sync.js';
13
+ const cli = program.command('files').helpGroup('Plugins:').description('CLI integration for @axium/storage');
14
+ cli.command('usage')
15
+ .description('Show your usage')
16
+ .action(async () => {
17
+ const { limits, usage } = await getUserStorageInfo(session().userId);
18
+ console.log(`Items: ${usage.items} ${limits.user_items ? ' / ' + limits.user_items : ''}`);
19
+ console.log(`Space: ${formatBytes(usage.bytes)} ${limits.user_size ? ' / ' + formatBytes(limits.user_size * 1_000_000) : ''}`);
20
+ });
21
+ cli.command('ls')
22
+ .alias('list')
23
+ .description('List the contents of a folder')
24
+ .action(() => {
25
+ io.error('Not implemented yet.');
26
+ });
27
+ cli.command('status')
28
+ .option('-v, --verbose', 'Show more details')
29
+ .option('--refresh', 'Force refresh metadata from the server')
30
+ .action(async (opt) => {
31
+ console.log(styleText('bold', `${config.sync.length} synced folder(s):`));
32
+ for (const sync of config.sync) {
33
+ if (opt.refresh)
34
+ await fetchSyncItems(sync.itemId, sync.name);
35
+ const delta = computeDelta(sync.itemId, sync.localPath);
36
+ if (opt.verbose) {
37
+ console.log(styleText('underline', sync.localPath + ':'));
38
+ if (delta.synced.length == delta.items.length && !delta.localOnly.length) {
39
+ console.log('\t' + styleText('blueBright', 'All files are synced!'));
40
+ continue;
41
+ }
42
+ for (const { _path } of delta.localOnly)
43
+ console.log('\t' + styleText('green', '+ ' + _path));
44
+ for (const { path } of delta.remoteOnly)
45
+ console.log('\t' + styleText('red', '- ' + path));
46
+ for (const { path, modifiedAt } of delta.modified) {
47
+ const outdated = modifiedAt.getTime() > statSync(join(sync.localPath, path)).mtime.getTime();
48
+ console.log('\t' + styleText('yellow', '~ ' + path) + (outdated ? ' (outdated)' : ''));
49
+ }
50
+ }
51
+ else {
52
+ console.log(sync.localPath + ':', Object.entries(delta)
53
+ .map(([name, items]) => `${styleText('blueBright', items.length.toString())} ${name}`)
54
+ .join(', '));
55
+ }
56
+ }
57
+ });
58
+ cli.command('add')
59
+ .description('Add a folder to be synced')
60
+ .argument('<path>', 'local path to the folder to sync')
61
+ .argument('<remote>', 'remote folder path')
62
+ .action(async (localPath, remoteName) => {
63
+ localPath = resolve(localPath);
64
+ for (const sync of config.sync) {
65
+ if (sync.localPath == localPath || localPath.startsWith(sync.localPath + '/'))
66
+ io.exit('This local path is already being synced.');
67
+ if (sync.remotePath == remoteName || remoteName.startsWith(sync.remotePath + '/'))
68
+ io.exit('This remote path is already being synced.');
69
+ }
70
+ const { userId } = session();
71
+ const local = await stat(localPath).catch(e => io.exit(e.toString()));
72
+ if (!local.isDirectory())
73
+ io.exit('Local path is not a directory.');
74
+ /**
75
+ * @todo Add an endpoint to fetch directories (maybe with the full paths?)
76
+ */
77
+ const allItems = remoteName.includes('/') ? (await getUserStorage(userId)).items : await getUserStorageRoot(userId);
78
+ const remote = walkItems(remoteName, allItems);
79
+ if (!remote)
80
+ io.exit('Could not resolve remote path.');
81
+ if (remote.type != 'inode/directory')
82
+ io.exit('Remote path is not a directory.');
83
+ config.sync.push({
84
+ name: remote.name,
85
+ itemId: remote.id,
86
+ localPath,
87
+ lastSynced: new Date(),
88
+ remotePath: remoteName,
89
+ });
90
+ await fetchSyncItems(remote.id);
91
+ saveConfig();
92
+ });
93
+ cli.command('unsync')
94
+ .alias('remove-sync')
95
+ .alias('rm-sync')
96
+ .description('Stop syncing a folder')
97
+ .argument('<path>', 'local path to the folder to stop syncing')
98
+ .action((localPath) => {
99
+ localPath = resolve(localPath);
100
+ const index = config.sync.findIndex(sync => sync.localPath == localPath);
101
+ if (index == -1)
102
+ io.exit('This local path is not being synced.');
103
+ unlinkSync(join(configDir, 'sync', config.sync[index].itemId + '.json'));
104
+ config.sync.splice(index, 1);
105
+ saveConfig();
106
+ });
107
+ cli.command('sync')
108
+ .description('Sync files')
109
+ .addOption(new Option('--delete', 'Delete local/remote files that were deleted remotely/locally')
110
+ .choices(['local', 'remote', 'none'])
111
+ .default('none'))
112
+ .option('-d, --dry-run', 'Show what would be done, but do not make any changes')
113
+ .argument('[sync]', 'The name of the Sync to sync')
114
+ .action(async (name, opt) => {
115
+ if (name) {
116
+ const sync = config.sync.find(s => s.name == name);
117
+ if (!sync)
118
+ io.exit('Can not find a Sync with that name.');
119
+ await doSync(sync, opt);
120
+ }
121
+ else
122
+ for (const sync of config.sync)
123
+ await doSync(sync, opt);
124
+ });
@@ -0,0 +1,15 @@
1
+ import * as z from 'zod';
2
+ declare const ClientStorageConfig: z.ZodObject<{
3
+ sync: z.ZodArray<z.ZodObject<{
4
+ name: z.ZodString;
5
+ itemId: z.ZodUUID;
6
+ localPath: z.ZodString;
7
+ lastSynced: z.ZodCoercedDate<unknown>;
8
+ remotePath: z.ZodString;
9
+ }, z.core.$strip>>;
10
+ }, z.core.$loose>;
11
+ export interface ClientStorageConfig extends z.infer<typeof ClientStorageConfig> {
12
+ }
13
+ export declare let config: ClientStorageConfig;
14
+ export declare function saveConfig(): void;
15
+ export {};
@@ -0,0 +1,21 @@
1
+ import { configDir } from '@axium/client/cli/config';
2
+ import { debug, writeJSON } from '@axium/core/node/io';
3
+ import { readFileSync } from 'node:fs';
4
+ import { join } from 'node:path/posix';
5
+ import * as z from 'zod';
6
+ import { Sync } from './sync.js';
7
+ const ClientStorageConfig = z.looseObject({
8
+ sync: Sync.array(),
9
+ });
10
+ const configPath = join(configDir, 'storage.json');
11
+ export let config = { sync: [] };
12
+ try {
13
+ config = ClientStorageConfig.parse(JSON.parse(readFileSync(configPath, 'utf-8')));
14
+ }
15
+ catch (e) {
16
+ debug('Could not load @axium/storage config: ' + (e instanceof z.core.$ZodError ? z.prettifyError(e) : e.message));
17
+ }
18
+ export function saveConfig() {
19
+ writeJSON(configPath, config);
20
+ debug('Saved @axium/storage config.');
21
+ }
@@ -0,0 +1 @@
1
+ export declare function run(): Promise<void>;
@@ -0,0 +1,6 @@
1
+ import { config } from './config.js';
2
+ import { doSync } from './sync.js';
3
+ export async function run() {
4
+ for (const sync of config.sync)
5
+ await doSync(sync, { delete: 'none', dryRun: false });
6
+ }
@@ -0,0 +1 @@
1
+ export * from './api.js';
@@ -0,0 +1 @@
1
+ export * from './api.js';
@@ -0,0 +1,2 @@
1
+ import type { StorageItemMetadata } from '../common.js';
2
+ export declare function walkItems(path: string, items: StorageItemMetadata[]): StorageItemMetadata | null;
@@ -0,0 +1,16 @@
1
+ export function walkItems(path, items) {
2
+ const resolved = [];
3
+ let currentItem = null, currentParentId = null;
4
+ const target = path.split('/').filter(p => p);
5
+ for (const part of target) {
6
+ for (const item of items) {
7
+ if (item.parentId != currentParentId || item.name != part)
8
+ continue;
9
+ currentItem = item;
10
+ currentParentId = item.id;
11
+ resolved.push(part);
12
+ break;
13
+ }
14
+ }
15
+ return currentItem;
16
+ }
@@ -0,0 +1,55 @@
1
+ import * as fs from 'node:fs';
2
+ import * as z from 'zod';
3
+ import '../polyfills.js';
4
+ /**
5
+ * A Sync is a storage item that has been selected for synchronization by the user.
6
+ * Importantly, it is *only* the "top-level" item.
7
+ * This means if a user selects a directory to sync, only that directory is a Sync.
8
+ *
9
+ * `Sync`s are client-side in nature, the server does not care about them.
10
+ * (we could do something fancy server-side at a later date)
11
+ */
12
+ export declare const Sync: z.ZodObject<{
13
+ name: z.ZodString;
14
+ itemId: z.ZodUUID;
15
+ localPath: z.ZodString;
16
+ lastSynced: z.ZodCoercedDate<unknown>;
17
+ remotePath: z.ZodString;
18
+ }, z.core.$strip>;
19
+ export interface Sync extends z.infer<typeof Sync> {
20
+ }
21
+ /** Local metadata about a storage item to sync */
22
+ export declare const LocalItem: z.ZodObject<{
23
+ id: z.ZodUUID;
24
+ path: z.ZodString;
25
+ modifiedAt: z.ZodCoercedDate<unknown>;
26
+ hash: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
27
+ }, z.core.$strip>;
28
+ export interface LocalItem extends z.infer<typeof LocalItem> {
29
+ }
30
+ export declare function fetchSyncItems(id: string, folderName?: string): Promise<LocalItem[]>;
31
+ export declare function getItems(id: string): LocalItem[];
32
+ export declare function setItems(id: string, items: LocalItem[]): void;
33
+ /** Metadata about a synced storage item. */
34
+ export interface ItemMetadata {
35
+ }
36
+ /** Computed metadata about changes */
37
+ export interface Delta {
38
+ synced: LocalItem[];
39
+ modified: LocalItem[];
40
+ remoteOnly: LocalItem[];
41
+ items: LocalItem[];
42
+ _items: Map<string, LocalItem>;
43
+ localOnly: (fs.Dirent<string> & {
44
+ _path: string;
45
+ })[];
46
+ }
47
+ /**
48
+ * Computes the changes between the local and remote, in the direction of the remote.
49
+ */
50
+ export declare function computeDelta(id: string, localPath: string): Delta;
51
+ export interface SyncOptions {
52
+ delete: 'local' | 'remote' | 'none';
53
+ dryRun: boolean;
54
+ }
55
+ export declare function doSync(sync: Sync, opt: SyncOptions): Promise<void>;
@@ -0,0 +1,205 @@
1
+ import { configDir, saveConfig } from '@axium/client/cli/config';
2
+ import { fetchAPI } from '@axium/client/requests';
3
+ import * as io from '@axium/core/node/io';
4
+ import { createHash } from 'node:crypto';
5
+ import * as fs from 'node:fs';
6
+ import { dirname, join, relative } from 'node:path';
7
+ import { pick } from 'utilium';
8
+ import * as z from 'zod';
9
+ import '../polyfills.js';
10
+ import { deleteItem, downloadItem, updateItem, uploadItem } from './api.js';
11
+ import mime from 'mime';
12
+ /**
13
+ * A Sync is a storage item that has been selected for synchronization by the user.
14
+ * Importantly, it is *only* the "top-level" item.
15
+ * This means if a user selects a directory to sync, only that directory is a Sync.
16
+ *
17
+ * `Sync`s are client-side in nature, the server does not care about them.
18
+ * (we could do something fancy server-side at a later date)
19
+ */
20
+ export const Sync = z.object({
21
+ name: z.string(),
22
+ itemId: z.uuid(),
23
+ localPath: z.string(),
24
+ lastSynced: z.coerce.date(),
25
+ remotePath: z.string(),
26
+ });
27
+ /** Local metadata about a storage item to sync */
28
+ export const LocalItem = z.object({
29
+ id: z.uuid(),
30
+ path: z.string(),
31
+ modifiedAt: z.coerce.date(),
32
+ hash: z.hex().length(128).nullish(),
33
+ });
34
+ export async function fetchSyncItems(id, folderName) {
35
+ io.start('Fetching ' + (folderName ?? id));
36
+ try {
37
+ const items = await fetchAPI('GET', 'storage/directory/:id/recursive', null, id);
38
+ const localItems = items.map(item => pick(item, 'id', 'path', 'modifiedAt', 'hash'));
39
+ fs.mkdirSync(join(configDir, 'sync'), { recursive: true });
40
+ io.writeJSON(join(configDir, 'sync', id + '.json'), localItems);
41
+ io.done();
42
+ return localItems;
43
+ }
44
+ catch (e) {
45
+ io.exit(e.message);
46
+ }
47
+ }
48
+ export function getItems(id) {
49
+ const items = JSON.parse(fs.readFileSync(join(configDir, 'sync', id + '.json'), 'utf-8'));
50
+ const { error, data } = LocalItem.array().safeParse(items);
51
+ if (error)
52
+ throw z.prettifyError(error);
53
+ return data;
54
+ }
55
+ export function setItems(id, items) {
56
+ fs.mkdirSync(join(configDir, 'sync'), { recursive: true });
57
+ io.writeJSON(join(configDir, 'sync', id + '.json'), items);
58
+ }
59
+ /**
60
+ * Computes the changes between the local and remote, in the direction of the remote.
61
+ */
62
+ export function computeDelta(id, localPath) {
63
+ const items = new Map(getItems(id).map(i => [i.path, i]));
64
+ const itemsSet = new Set(items.keys());
65
+ const files = new Map(fs.readdirSync(localPath, { recursive: true, encoding: 'utf8', withFileTypes: true }).map(d => {
66
+ const _path = relative(localPath, join(d.parentPath, d.name));
67
+ return [_path, Object.assign(d, { _path })];
68
+ }));
69
+ const synced = itemsSet.intersection(files);
70
+ const modified = new Set(synced.keys().filter(path => {
71
+ const full = join(localPath, path);
72
+ const hash = fs.statSync(full).isDirectory() ? null : createHash('BLAKE2b512').update(fs.readFileSync(full)).digest().toHex();
73
+ return hash != items.get(path)?.hash;
74
+ }));
75
+ const delta = {
76
+ _items: items,
77
+ items: Array.from(items.values()),
78
+ synced: Array.from(synced.difference(modified)).map(p => items.get(p)),
79
+ modified: Array.from(modified).map(p => items.get(p)),
80
+ localOnly: Array.from(new Set(files.keys()).difference(items)).map(p => files.get(p)),
81
+ remoteOnly: Array.from(itemsSet.difference(files)).map(p => items.get(p)),
82
+ };
83
+ Object.defineProperty(delta, '_items', { enumerable: false });
84
+ return delta;
85
+ }
86
+ export async function doSync(sync, opt) {
87
+ await fetchSyncItems(sync.itemId, sync.name);
88
+ const delta = computeDelta(sync.itemId, sync.localPath);
89
+ const { _items } = delta;
90
+ if (opt.delete == 'local')
91
+ delta.localOnly.reverse(); // so directories come after
92
+ for (const dirent of delta.localOnly) {
93
+ if (opt.delete == 'local') {
94
+ io.start('Deleting ' + dirent._path);
95
+ try {
96
+ if (opt.dryRun)
97
+ process.stdout.write('(dry run) ');
98
+ else {
99
+ fs.unlinkSync(join(sync.localPath, dirent._path));
100
+ _items.delete(dirent._path);
101
+ }
102
+ io.done();
103
+ }
104
+ catch (e) {
105
+ io.error(e.message);
106
+ }
107
+ continue;
108
+ }
109
+ const uploadOpts = {
110
+ parentId: dirname(dirent._path) == '.' ? sync.itemId : _items.get(dirname(dirent._path))?.id,
111
+ name: dirent.name,
112
+ };
113
+ io.start('Uploading ' + dirent._path);
114
+ try {
115
+ if (opt.dryRun)
116
+ process.stdout.write('(dry run) ');
117
+ else if (dirent.isDirectory()) {
118
+ const dir = await uploadItem(new Blob([], { type: 'inode/directory' }), uploadOpts);
119
+ _items.set(dirent._path, Object.assign(pick(dir, 'id', 'modifiedAt'), { path: dirent._path, hash: null }));
120
+ }
121
+ else {
122
+ const type = mime.getType(dirent._path) || 'application/octet-stream';
123
+ const content = fs.readFileSync(join(sync.localPath, dirent._path));
124
+ const file = await uploadItem(new Blob([content], { type }), uploadOpts);
125
+ _items.set(dirent._path, Object.assign(pick(file, 'id', 'modifiedAt', 'hash'), { path: dirent._path }));
126
+ }
127
+ io.done();
128
+ }
129
+ catch (e) {
130
+ io.error(e.message);
131
+ }
132
+ }
133
+ if (opt.delete == 'remote')
134
+ delta.remoteOnly.reverse();
135
+ for (const item of delta.remoteOnly) {
136
+ if (opt.delete == 'remote') {
137
+ io.start('Deleting ' + item.path);
138
+ try {
139
+ if (opt.dryRun)
140
+ process.stdout.write('(dry run) ');
141
+ else {
142
+ await deleteItem(item.id);
143
+ _items.delete(item.path);
144
+ }
145
+ io.done();
146
+ }
147
+ catch (e) {
148
+ io.error(e.message);
149
+ }
150
+ continue;
151
+ }
152
+ io.start('Downloading ' + item.path);
153
+ try {
154
+ const fullPath = join(sync.localPath, item.path);
155
+ if (opt.dryRun)
156
+ process.stdout.write('(dry run) ');
157
+ else if (!item.hash) {
158
+ fs.mkdirSync(fullPath, { recursive: true });
159
+ }
160
+ else {
161
+ const blob = await downloadItem(item.id);
162
+ const content = await blob.bytes();
163
+ fs.writeFileSync(fullPath, content);
164
+ }
165
+ io.done();
166
+ }
167
+ catch (e) {
168
+ io.error(e.message);
169
+ }
170
+ }
171
+ for (const item of delta.modified) {
172
+ io.start('Updating ' + item.path);
173
+ try {
174
+ const content = fs.readFileSync(join(sync.localPath, item.path));
175
+ const type = mime.getType(item.path) || 'application/octet-stream';
176
+ if (item.modifiedAt.getTime() > fs.statSync(join(sync.localPath, item.path)).mtime.getTime()) {
177
+ if (opt.dryRun)
178
+ process.stdout.write('(dry run) ');
179
+ else {
180
+ const blob = await downloadItem(item.id);
181
+ const content = await blob.bytes();
182
+ fs.writeFileSync(join(sync.localPath, item.path), content);
183
+ }
184
+ console.log('server.');
185
+ }
186
+ else {
187
+ if (opt.dryRun)
188
+ process.stdout.write('(dry run) ');
189
+ else {
190
+ const updated = await updateItem(item.id, new Blob([content], { type }));
191
+ _items.set(item.path, Object.assign(pick(updated, 'id', 'modifiedAt', 'hash'), { path: item.path }));
192
+ }
193
+ console.log('local.');
194
+ }
195
+ }
196
+ catch (e) {
197
+ io.error(e.message);
198
+ }
199
+ }
200
+ if (opt.dryRun)
201
+ return;
202
+ setItems(sync.itemId, Array.from(_items.values()));
203
+ sync.lastSynced = new Date();
204
+ saveConfig();
205
+ }
package/dist/common.d.ts CHANGED
@@ -14,6 +14,15 @@ declare module '@axium/core/api' {
14
14
  'users/:id/storage/shared': {
15
15
  GET: StorageItemMetadata[];
16
16
  };
17
+ storage: {
18
+ OPTIONS: StoragePublicConfig & {
19
+ syncProtocolVersion: number;
20
+ batchFormatVersion: number;
21
+ };
22
+ };
23
+ 'storage/batch': {
24
+ POST: [StorageBatchUpdate[], StorageItemMetadata[]];
25
+ };
17
26
  'storage/item/:id': {
18
27
  GET: StorageItemMetadata;
19
28
  DELETE: StorageItemMetadata;
@@ -22,8 +31,31 @@ declare module '@axium/core/api' {
22
31
  'storage/directory/:id': {
23
32
  GET: StorageItemMetadata[];
24
33
  };
34
+ 'storage/directory/:id/recursive': {
35
+ GET: (StorageItemMetadata & {
36
+ path: string;
37
+ })[];
38
+ };
25
39
  }
26
40
  }
41
+ export interface StoragePublicConfig {
42
+ /** Configuration for batch updates */
43
+ batch: {
44
+ /** Whether to enable sending multiple files per request */
45
+ enabled: boolean;
46
+ /** Maximum number of items that can be included in a single batch update */
47
+ max_items: number;
48
+ /** Maximum size in KiB per item */
49
+ max_item_size: number;
50
+ };
51
+ /** Whether to split files larger than `max_transfer_size` into multiple chunks */
52
+ chunk: boolean;
53
+ /** Maximum size in MiB per transfer/request */
54
+ max_transfer_size: number;
55
+ /** Maximum number of chunks */
56
+ max_chunks: number;
57
+ }
58
+ export declare const syncProtocolVersion = 0;
27
59
  export interface StorageLimits {
28
60
  /** The maximum size per item in MB */
29
61
  item_size: number;
@@ -70,3 +102,31 @@ export interface StorageItemMetadata<T extends Record<string, unknown> = Record<
70
102
  type: string;
71
103
  metadata: T;
72
104
  }
105
+ /**
106
+ * Formats:
107
+ *
108
+ * **v0**:
109
+ * - Metadata transferred using JSON
110
+ * - `x-batch-header-size` HTTP header used to determine batch header size
111
+ * - Binary data appended after batch header
112
+ */
113
+ export declare const batchFormatVersion = 0;
114
+ export declare const BatchedContentChange: z.ZodObject<{
115
+ offset: z.ZodInt;
116
+ size: z.ZodInt;
117
+ }, z.core.$strip>;
118
+ export declare const StorageBatchUpdate: z.ZodObject<{
119
+ deleted: z.ZodArray<z.ZodUUID>;
120
+ metadata: z.ZodRecord<z.ZodUUID, z.ZodObject<{
121
+ name: z.ZodOptional<z.ZodString>;
122
+ owner: z.ZodOptional<z.ZodUUID>;
123
+ trash: z.ZodOptional<z.ZodBoolean>;
124
+ publicPermission: z.ZodOptional<z.ZodNumber>;
125
+ }, z.core.$strip>>;
126
+ content: z.ZodRecord<z.ZodUUID, z.ZodObject<{
127
+ offset: z.ZodInt;
128
+ size: z.ZodInt;
129
+ }, z.core.$strip>>;
130
+ }, z.core.$strip>;
131
+ export interface StorageBatchUpdate extends z.infer<typeof StorageBatchUpdate> {
132
+ }
package/dist/common.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as z from 'zod';
2
+ export const syncProtocolVersion = 0;
2
3
  /**
3
4
  * An update to file metadata.
4
5
  */
@@ -10,3 +11,22 @@ export const StorageItemUpdate = z
10
11
  publicPermission: z.number().min(0).max(5),
11
12
  })
12
13
  .partial();
14
+ /**
15
+ * Formats:
16
+ *
17
+ * **v0**:
18
+ * - Metadata transferred using JSON
19
+ * - `x-batch-header-size` HTTP header used to determine batch header size
20
+ * - Binary data appended after batch header
21
+ */
22
+ export const batchFormatVersion = 0;
23
+ export const BatchedContentChange = z.object({
24
+ /** Offset in request body */
25
+ offset: z.int().nonnegative(),
26
+ size: z.int().nonnegative(),
27
+ });
28
+ export const StorageBatchUpdate = z.object({
29
+ deleted: z.uuid().array(),
30
+ metadata: z.record(z.uuid(), StorageItemUpdate),
31
+ content: z.record(z.uuid(), BatchedContentChange),
32
+ });
@@ -24,7 +24,7 @@ declare global {
24
24
  */
25
25
  fromHex: (string: string) => Uint8Array;
26
26
  }
27
- interface Uint8Array<TArrayBuffer extends ArrayBufferLike> {
27
+ interface Uint8Array {
28
28
  /**
29
29
  * Converts the `Uint8Array` to a base64-encoded string.
30
30
  * @param options If provided, sets the alphabet and padding behavior used.
package/dist/polyfills.js CHANGED
@@ -7,14 +7,14 @@ https://github.com/microsoft/TypeScript/issues/61695
7
7
 
8
8
  @todo Remove when TypeScript 5.9 is released
9
9
  */
10
- import { output } from '@axium/server/io';
10
+ import { debug } from '@axium/core/node/io';
11
11
  Uint8Array.prototype.toHex ??=
12
- (output.warn('Using a polyfill of Uint8Array.prototype.toHex'),
12
+ (debug('Using a polyfill of Uint8Array.prototype.toHex'),
13
13
  function toHex() {
14
14
  return [...this].map(b => b.toString(16).padStart(2, '0')).join('');
15
15
  });
16
16
  Uint8Array.fromHex ??=
17
- (output.warn('Using a polyfill of Uint8Array.fromHex'),
17
+ (debug('Using a polyfill of Uint8Array.fromHex'),
18
18
  function fromHex(hex) {
19
19
  const bytes = new Uint8Array(hex.length / 2);
20
20
  for (let i = 0; i < hex.length; i += 2) {
@@ -0,0 +1 @@
1
+ import '../polyfills.js';