pyric-admin 0.1.0-alpha.30.2 → 0.1.0-alpha.32
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/dist/database/index.d.ts +6 -1
- package/dist/database/index.d.ts.map +1 -1
- package/dist/database/index.js +119 -65
- package/dist/database/index.js.map +1 -1
- package/dist/storage/delete-files.d.ts +6 -0
- package/dist/storage/delete-files.d.ts.map +1 -1
- package/dist/storage/delete-files.js +21 -1
- package/dist/storage/delete-files.js.map +1 -1
- package/dist/storage/file-query.d.ts +12 -0
- package/dist/storage/file-query.d.ts.map +1 -1
- package/dist/storage/file-query.js +22 -12
- package/dist/storage/file-query.js.map +1 -1
- package/dist/storage/get-files.d.ts +6 -0
- package/dist/storage/get-files.d.ts.map +1 -1
- package/dist/storage/get-files.js +24 -1
- package/dist/storage/get-files.js.map +1 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +11 -9
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/remote-names.d.ts +28 -4
- package/dist/storage/remote-names.d.ts.map +1 -1
- package/dist/storage/remote-names.js +125 -13
- package/dist/storage/remote-names.js.map +1 -1
- package/package.json +2 -2
- package/src/auth/oracle-admin-answers.test.ts +114 -0
- package/src/auth/verify-id-token-claims.test.ts +42 -0
- package/src/database/index.ts +136 -68
- package/src/storage/delete-files.ts +20 -1
- package/src/storage/file-query.ts +24 -12
- package/src/storage/get-files.ts +25 -0
- package/src/storage/index.ts +11 -9
- package/src/storage/remote-names.ts +144 -13
package/src/storage/index.ts
CHANGED
|
@@ -93,10 +93,10 @@ import {
|
|
|
93
93
|
import { assertAdminAppActive } from '../app/lifecycle.js';
|
|
94
94
|
import { defineDeferredMembers } from 'pyric/app/internal';
|
|
95
95
|
import { BUCKET_DEFERRED_MEMBERS, FILE_DEFERRED_MEMBERS } from './deferred-members.js';
|
|
96
|
-
import { deleteListedFiles } from './delete-files.js';
|
|
96
|
+
import { deletedNamesFound, deleteListedFiles } from './delete-files.js';
|
|
97
97
|
import { assertQuery, type DeleteFilesOptions, type GetFilesOptions } from './file-query.js';
|
|
98
|
-
import { getFilesResponse, type GetFilesResponse } from './get-files.js';
|
|
99
|
-
import {
|
|
98
|
+
import { getFilesResponse, pageNamesFound, type GetFilesResponse } from './get-files.js';
|
|
99
|
+
import { namesForQuery, type FolderListing } from './remote-names.js';
|
|
100
100
|
|
|
101
101
|
export type { DeleteFilesOptions, GetFilesApiResponse, GetFilesOptions } from './file-query.js';
|
|
102
102
|
export type { GetFilesResponse } from './get-files.js';
|
|
@@ -793,22 +793,24 @@ class RemoteBucket implements Bucket {
|
|
|
793
793
|
|
|
794
794
|
async getFiles(query: GetFilesOptions = {}): Promise<GetFilesResponse<File>> {
|
|
795
795
|
assertQuery(query, 'getFiles');
|
|
796
|
-
|
|
796
|
+
const names = await this.namesFor(query, pageNamesFound(query));
|
|
797
|
+
return getFilesResponse(names, query, name => this.file(name));
|
|
797
798
|
}
|
|
798
799
|
|
|
799
800
|
async deleteFiles(query: DeleteFilesOptions = {}): Promise<void> {
|
|
800
801
|
assertQuery(query, 'deleteFiles');
|
|
801
|
-
|
|
802
|
+
const names = await this.namesFor(query, deletedNamesFound(query));
|
|
803
|
+
await deleteListedFiles(names, query, name => this.file(name).delete());
|
|
802
804
|
}
|
|
803
805
|
|
|
804
|
-
/** The host's object names
|
|
805
|
-
private
|
|
806
|
-
return
|
|
806
|
+
/** The host's object names `query` reads, walked folder by folder with `storage.listAll` until `enough`. */
|
|
807
|
+
private namesFor(query: GetFilesOptions, enough: (name: string) => boolean): Promise<string[]> {
|
|
808
|
+
return namesForQuery(query, async path => (await this.channel.op({
|
|
807
809
|
method: 'storage.listAll',
|
|
808
810
|
...this.wire,
|
|
809
811
|
path,
|
|
810
812
|
actAs: STORAGE_REMOTE_ADMIN_LENS,
|
|
811
|
-
})) as FolderListing);
|
|
813
|
+
})) as FolderListing, enough);
|
|
812
814
|
}
|
|
813
815
|
}
|
|
814
816
|
|
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The object names
|
|
3
|
-
* host's `storage.listAll` op. That op lists one folder: its files and its
|
|
2
|
+
* The object names a `getFiles` query reads on a remote sandbox's host, with
|
|
3
|
+
* the host's `storage.listAll` op. That op lists one folder: its files and its
|
|
4
4
|
* subfolders. A `getFiles` prefix is not a folder (`users/12` matches
|
|
5
5
|
* `users/123/a.png`), so the walk starts at the folder holding the prefix and
|
|
6
6
|
* enters each subfolder whose path can hold a matching name.
|
|
7
|
+
*
|
|
8
|
+
* The walk goes in name order, depth first, with a few folder listings in
|
|
9
|
+
* flight at once (#1183): a client may have 256 pending operations, and a
|
|
10
|
+
* store with a folder per object has more folders than that. Because names
|
|
11
|
+
* come in order, a query that needs one page stops once it has that page.
|
|
12
|
+
*
|
|
13
|
+
* - Name order: every name in folder `f` starts with `f/`, and no file in
|
|
14
|
+
* the parent sorts between two such names, so visiting a folder's files
|
|
15
|
+
* and subfolders sorted by `name` and `path + '/'` yields names in order.
|
|
16
|
+
* - A subfolder whose names all roll up to one prefix at the delimiter is
|
|
17
|
+
* not entered: the walk yields that prefix in place of its names, which
|
|
18
|
+
* the listing reports as a prefix, never as a file.
|
|
19
|
+
* - A subfolder whose names all sort at or before the `pageToken` is not
|
|
20
|
+
* entered: every name in `f` sorts before `f0`.
|
|
7
21
|
*/
|
|
22
|
+
import { compareKeys, entryOf, resumesAfter, type GetFilesOptions } from './file-query.js';
|
|
8
23
|
|
|
9
24
|
/** A folder's listing, as the host's `storage.listAll` answers it. */
|
|
10
25
|
export interface FolderListing {
|
|
@@ -12,22 +27,138 @@ export interface FolderListing {
|
|
|
12
27
|
prefixes: Array<{ fullPath: string }>;
|
|
13
28
|
}
|
|
14
29
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
30
|
+
export type ListFolder = (path: string) => Promise<FolderListing>;
|
|
31
|
+
|
|
32
|
+
/** How many folder listings the walk keeps in flight, well under the 256 a client may have pending. */
|
|
33
|
+
export const WALK_CONCURRENCY = 8;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The names under `query`'s prefix, in name order, listing folders with
|
|
37
|
+
* `listFolder`. `enough` sees each name as it is found and stops the walk
|
|
38
|
+
* once it answers true.
|
|
39
|
+
*/
|
|
40
|
+
export async function namesForQuery(
|
|
41
|
+
query: GetFilesOptions,
|
|
42
|
+
listFolder: ListFolder,
|
|
43
|
+
enough: (name: string) => boolean = () => false,
|
|
19
44
|
): Promise<string[]> {
|
|
20
45
|
const names: string[] = [];
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
await Promise.all(entered.map(walk));
|
|
26
|
-
};
|
|
27
|
-
await walk(prefix.slice(0, Math.max(prefix.lastIndexOf('/'), 0)));
|
|
46
|
+
for await (const name of walkNames(query, listFolder)) {
|
|
47
|
+
names.push(name);
|
|
48
|
+
if (enough(name)) break;
|
|
49
|
+
}
|
|
28
50
|
return names;
|
|
29
51
|
}
|
|
30
52
|
|
|
53
|
+
interface FolderNode {
|
|
54
|
+
kind: 'folder';
|
|
55
|
+
path: string;
|
|
56
|
+
listing?: Promise<FolderListing>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type WalkNode = { kind: 'names'; names: string[] } | FolderNode;
|
|
60
|
+
|
|
61
|
+
/** Yield the names under `query`'s prefix in name order, `concurrency` listings at a time at most. */
|
|
62
|
+
export async function* walkNames(
|
|
63
|
+
query: GetFilesOptions,
|
|
64
|
+
listFolder: ListFolder,
|
|
65
|
+
concurrency = WALK_CONCURRENCY,
|
|
66
|
+
): AsyncGenerator<string> {
|
|
67
|
+
const prefix = query.prefix ?? '';
|
|
68
|
+
const start = prefix.slice(0, Math.max(prefix.lastIndexOf('/'), 0));
|
|
69
|
+
let inFlight = 0;
|
|
70
|
+
let onSettle: Array<() => void> = [];
|
|
71
|
+
const aSlotFrees = (): Promise<void> => new Promise(resolve => onSettle.push(resolve));
|
|
72
|
+
const startListing = (node: FolderNode): Promise<FolderListing> => {
|
|
73
|
+
inFlight += 1;
|
|
74
|
+
const listing = listFolder(node.path);
|
|
75
|
+
// Count the listing until it settles. Handling both outcomes here also
|
|
76
|
+
// marks a rejection handled, so a walk that stops before reaching a
|
|
77
|
+
// looked-ahead folder leaves no unhandled rejection.
|
|
78
|
+
const settled = (): void => {
|
|
79
|
+
inFlight -= 1;
|
|
80
|
+
const waiting = onSettle;
|
|
81
|
+
onSettle = [];
|
|
82
|
+
for (const resolve of waiting) resolve();
|
|
83
|
+
};
|
|
84
|
+
listing.then(settled, settled);
|
|
85
|
+
node.listing = listing;
|
|
86
|
+
return listing;
|
|
87
|
+
};
|
|
88
|
+
// The next node is at the end; nodes below it come later in name order.
|
|
89
|
+
const stack: WalkNode[] = [{ kind: 'folder', path: start }];
|
|
90
|
+
for (let node = stack.pop(); node !== undefined; node = stack.pop()) {
|
|
91
|
+
if (node.kind === 'names') {
|
|
92
|
+
yield* node.names;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
// Lookahead may hold every slot; this folder's listing waits for one.
|
|
96
|
+
while (node.listing === undefined && inFlight >= concurrency) await aSlotFrees();
|
|
97
|
+
const listing = node.listing ?? startListing(node);
|
|
98
|
+
listAhead(stack, concurrency, () => inFlight < concurrency, startListing);
|
|
99
|
+
stack.push(...childrenOf(await listing, query).reverse());
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Start the listings of the next folders in name order while `hasRoom`,
|
|
105
|
+
* looking at most `window` folders ahead. A listing started here is awaited
|
|
106
|
+
* when its folder comes up.
|
|
107
|
+
*/
|
|
108
|
+
function listAhead(
|
|
109
|
+
stack: WalkNode[],
|
|
110
|
+
window: number,
|
|
111
|
+
hasRoom: () => boolean,
|
|
112
|
+
startListing: (node: FolderNode) => Promise<FolderListing>,
|
|
113
|
+
): void {
|
|
114
|
+
let seen = 0;
|
|
115
|
+
for (let index = stack.length - 1; index >= 0 && seen < window && hasRoom(); index -= 1) {
|
|
116
|
+
const node = stack[index]!;
|
|
117
|
+
if (node.kind !== 'folder') continue;
|
|
118
|
+
seen += 1;
|
|
119
|
+
if (node.listing === undefined) void startListing(node);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** A folder's files and the subfolders worth entering, in name order, with runs of names grouped. */
|
|
124
|
+
function childrenOf(listing: FolderListing, query: GetFilesOptions): WalkNode[] {
|
|
125
|
+
const prefix = query.prefix ?? '';
|
|
126
|
+
const after = resumesAfter(query);
|
|
127
|
+
const children: Array<{ key: string; node: WalkNode }> = [];
|
|
128
|
+
for (const { fullPath } of listing.items) {
|
|
129
|
+
if (fullPath.startsWith(prefix)) children.push({ key: fullPath, node: { kind: 'names', names: [fullPath] } });
|
|
130
|
+
}
|
|
131
|
+
for (const { fullPath } of listing.prefixes) {
|
|
132
|
+
const key = `${fullPath}/`;
|
|
133
|
+
const holdsMatch = canHoldMatch(key, prefix);
|
|
134
|
+
const passedByToken = after !== undefined && `${fullPath}0` <= after;
|
|
135
|
+
if (!holdsMatch || passedByToken) continue;
|
|
136
|
+
const rolledUp = rollUpOf(key, query);
|
|
137
|
+
const node: WalkNode = rolledUp === undefined ? { kind: 'folder', path: fullPath } : { kind: 'names', names: [rolledUp] };
|
|
138
|
+
children.push({ key, node });
|
|
139
|
+
}
|
|
140
|
+
children.sort((a, b) => compareKeys(a.key, b.key));
|
|
141
|
+
return groupNames(children.map(child => child.node));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The prefix every name in the folder `folderWithSlash` rolls up to, if they all roll up to one. */
|
|
145
|
+
function rollUpOf(folderWithSlash: string, query: GetFilesOptions): string | undefined {
|
|
146
|
+
const entry = entryOf(folderWithSlash, query);
|
|
147
|
+
return entry?.rolledUp === true ? entry.key : undefined;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Merge each run of adjacent name nodes into one, so looking ahead skips a run in one step. */
|
|
151
|
+
function groupNames(nodes: WalkNode[]): WalkNode[] {
|
|
152
|
+
const grouped: WalkNode[] = [];
|
|
153
|
+
for (const node of nodes) {
|
|
154
|
+
const last = grouped[grouped.length - 1];
|
|
155
|
+
const joinsRun = node.kind === 'names' && last?.kind === 'names';
|
|
156
|
+
if (joinsRun) last.names.push(...node.names);
|
|
157
|
+
else grouped.push(node);
|
|
158
|
+
}
|
|
159
|
+
return grouped;
|
|
160
|
+
}
|
|
161
|
+
|
|
31
162
|
/** Can a name in the folder `folderWithSlash` start with `prefix`? */
|
|
32
163
|
function canHoldMatch(folderWithSlash: string, prefix: string): boolean {
|
|
33
164
|
return folderWithSlash.startsWith(prefix) || prefix.startsWith(folderWithSlash);
|