mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-24 15:07:17 +00:00
fix(fs): give a new entry to the owner of the folder it lands in
A file a share recipient added to a shared folder was recorded as theirs while living in the owner's tree, so a subtree could hold rows belonging to several people — and the storage it consumed was checked against the writer while being counted against the owner. Take the owner from the parent row at every insert, charge the allowance to that owner, and hand a moved entry over to the tree it moves into. An entry now always belongs to whoever owns the directory holding it.
This commit is contained in:
@@ -2920,6 +2920,223 @@ describe('FSService restructuring a shared tree', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('FSService ownership in a shared tree', () => {
|
||||
let owner: TestUser;
|
||||
let holder: TestUser;
|
||||
let shared: FSEntry;
|
||||
|
||||
const asHolder = <T>(run: () => Promise<T>): Promise<T> =>
|
||||
runWithContext({ actor: holder.actor }, run);
|
||||
|
||||
beforeAll(async () => {
|
||||
owner = await makeUser();
|
||||
holder = await makeUser();
|
||||
shared = await fs.mkdir(owner.userId, {
|
||||
path: `${owner.home}/Documents/Team`,
|
||||
});
|
||||
await server.services.acl.setUserUser(
|
||||
owner.actor,
|
||||
holder.actor,
|
||||
{
|
||||
path: shared.path,
|
||||
resolveAncestors: () => fs.getAncestorChain(shared.path),
|
||||
},
|
||||
'write',
|
||||
);
|
||||
});
|
||||
|
||||
it('gives a folder the recipient creates to the folder owner', async () => {
|
||||
const created = await asHolder(() =>
|
||||
fs.mkdir(holder.userId, { path: `${shared.path}/from-holder` }),
|
||||
);
|
||||
|
||||
expect(created.userId).toBe(owner.userId);
|
||||
});
|
||||
|
||||
it('gives a file the recipient writes to the folder owner', async () => {
|
||||
const created = await asHolder(() =>
|
||||
fs.write(holder.userId, {
|
||||
fileMetadata: {
|
||||
path: `${shared.path}/note.txt`,
|
||||
size: 1,
|
||||
contentType: 'text/plain',
|
||||
},
|
||||
fileContent: 'x',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(created.fsEntry.userId).toBe(owner.userId);
|
||||
});
|
||||
|
||||
it('gives a file the recipient touches to the folder owner', async () => {
|
||||
const created = await asHolder(() =>
|
||||
fs.touch(holder.userId, { path: `${shared.path}/touched.txt` }),
|
||||
);
|
||||
|
||||
expect(created.userId).toBe(owner.userId);
|
||||
});
|
||||
|
||||
it('gives intermediate directories to the folder owner', async () => {
|
||||
await asHolder(() =>
|
||||
fs.write(holder.userId, {
|
||||
fileMetadata: {
|
||||
path: `${shared.path}/a/b/deep.txt`,
|
||||
size: 1,
|
||||
contentType: 'text/plain',
|
||||
createMissingParents: true,
|
||||
},
|
||||
fileContent: 'x',
|
||||
}),
|
||||
);
|
||||
|
||||
const a = await server.stores.fsEntry.getEntryByPath(
|
||||
`${shared.path}/a`,
|
||||
);
|
||||
const b = await server.stores.fsEntry.getEntryByPath(
|
||||
`${shared.path}/a/b`,
|
||||
);
|
||||
expect(a?.userId).toBe(owner.userId);
|
||||
expect(b?.userId).toBe(owner.userId);
|
||||
});
|
||||
|
||||
it('gives a copy the recipient makes to the folder owner', async () => {
|
||||
const source = await writeFile(
|
||||
holder,
|
||||
`${holder.home}/Documents/mine.txt`,
|
||||
'x',
|
||||
);
|
||||
|
||||
const copy = await asHolder(() =>
|
||||
fs.copy(holder.userId, {
|
||||
source,
|
||||
destinationParent: shared,
|
||||
newName: 'copied.txt',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(copy.userId).toBe(owner.userId);
|
||||
// The original stays where it was, with its own owner.
|
||||
expect(
|
||||
(await server.stores.fsEntry.getEntryByPath(source.path))?.userId,
|
||||
).toBe(holder.userId);
|
||||
});
|
||||
|
||||
it('hands over an entry the recipient moves in, subtree and all', async () => {
|
||||
const dir = await fs.mkdir(holder.userId, {
|
||||
path: `${holder.home}/Documents/handover`,
|
||||
});
|
||||
await writeFile(
|
||||
holder,
|
||||
`${holder.home}/Documents/handover/inside.txt`,
|
||||
'x',
|
||||
);
|
||||
|
||||
const moved = await asHolder(() =>
|
||||
fs.move(holder.userId, { source: dir, destinationParent: shared }),
|
||||
);
|
||||
|
||||
expect(moved.userId).toBe(owner.userId);
|
||||
const inside = await server.stores.fsEntry.getEntryByPath(
|
||||
`${shared.path}/handover/inside.txt`,
|
||||
{ skipCache: true },
|
||||
);
|
||||
expect(inside?.userId).toBe(owner.userId);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('FSService storage allowance in a shared tree', () => {
|
||||
let limitedServer: PuterServer;
|
||||
let limitedFs: FSService;
|
||||
|
||||
beforeAll(async () => {
|
||||
limitedServer = await setupTestServer({
|
||||
is_storage_limited: true,
|
||||
} as never);
|
||||
limitedFs = limitedServer.services.fs as unknown as FSService;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await limitedServer?.shutdown();
|
||||
});
|
||||
|
||||
const quotaUser = async (freeStorage: number) => {
|
||||
const username = `fsqs-${Math.random().toString(36).slice(2, 10)}`;
|
||||
const created = await limitedServer.stores.user.create({
|
||||
username,
|
||||
uuid: uuidv4(),
|
||||
password: null,
|
||||
email: `${username}@test.local`,
|
||||
free_storage: freeStorage,
|
||||
requires_email_confirmation: false,
|
||||
});
|
||||
await generateDefaultFsentries(
|
||||
limitedServer.clients.db,
|
||||
limitedServer.stores.user,
|
||||
created,
|
||||
);
|
||||
const refreshed = (await limitedServer.stores.user.getById(created.id))!;
|
||||
return {
|
||||
userId: refreshed.id,
|
||||
home: `/${username}`,
|
||||
actor: {
|
||||
user: {
|
||||
id: refreshed.id,
|
||||
uuid: refreshed.uuid,
|
||||
username: refreshed.username,
|
||||
email: refreshed.email ?? null,
|
||||
email_confirmed: true,
|
||||
} as Actor['user'],
|
||||
} as Actor,
|
||||
};
|
||||
};
|
||||
|
||||
it('charges the folder owner, so their limit stops the writer', async () => {
|
||||
const owner = await quotaUser(64);
|
||||
const holder = await quotaUser(10 * 1024 * 1024);
|
||||
const shared = await limitedFs.mkdir(owner.userId, {
|
||||
path: `${owner.home}/Documents/Tight`,
|
||||
});
|
||||
await limitedServer.services.acl.setUserUser(
|
||||
owner.actor,
|
||||
holder.actor,
|
||||
{
|
||||
path: shared.path,
|
||||
resolveAncestors: () =>
|
||||
limitedFs.getAncestorChain(shared.path),
|
||||
},
|
||||
'write',
|
||||
);
|
||||
|
||||
const body = 'x'.repeat(4096);
|
||||
const error = await caught(() =>
|
||||
runWithContext({ actor: holder.actor }, () =>
|
||||
limitedFs.write(holder.userId, {
|
||||
fileMetadata: {
|
||||
path: `${shared.path}/big.txt`,
|
||||
size: body.length,
|
||||
},
|
||||
fileContent: body,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
expect(error.statusCode).toBe(413);
|
||||
expect(error.legacyCode).toBe('storage_limit_reached');
|
||||
// The same write into the writer's own roomy home still goes through,
|
||||
// so it was the owner's limit that stopped it, not a blanket refusal.
|
||||
await runWithContext({ actor: holder.actor }, () =>
|
||||
limitedFs.write(holder.userId, {
|
||||
fileMetadata: {
|
||||
path: `${holder.home}/Documents/big.txt`,
|
||||
size: body.length,
|
||||
},
|
||||
fileContent: body,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('FSService access checks', () => {
|
||||
let owner: TestUser;
|
||||
let stranger: TestUser;
|
||||
|
||||
@@ -743,6 +743,17 @@ export class FSService extends PuterService {
|
||||
return Math.max(allowanceMax, storageAllowanceMaxOverride);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whose allowance a write to `path` draws on — the owner of the tree it
|
||||
* lands in, which is not the writer when the folder was shared with them.
|
||||
*/
|
||||
async #storageOwnerOf(path: string, actingUserId: number): Promise<number> {
|
||||
const home = `/${this.#normalizePath(path).split('/')[1] ?? ''}`;
|
||||
if (home === '/') return actingUserId;
|
||||
const entry = await this.stores.fsEntry.getEntryByPath(home);
|
||||
return entry?.userId ?? actingUserId;
|
||||
}
|
||||
|
||||
async #assertStorageAllowance(
|
||||
userId: number,
|
||||
incomingSize: number,
|
||||
@@ -1409,23 +1420,36 @@ export class FSService extends PuterService {
|
||||
}
|
||||
}
|
||||
|
||||
const sizeChanges = preparedBatch.items.map((item) => {
|
||||
// One batch can straddle two trees, and each owner pays for its own.
|
||||
const sizeChangesByOwner = new Map<
|
||||
number,
|
||||
Array<{ incomingSize: number; existingSize: number }>
|
||||
>();
|
||||
for (const item of preparedBatch.items) {
|
||||
const uploadedItem = uploadedItemMap.get(item.index);
|
||||
return {
|
||||
const owner = await this.#storageOwnerOf(
|
||||
item.normalizedInput.path,
|
||||
preparedBatch.userId,
|
||||
);
|
||||
const sizeChanges = sizeChangesByOwner.get(owner) ?? [];
|
||||
sizeChanges.push({
|
||||
incomingSize: uploadedItem
|
||||
? uploadedItem.uploadedSize
|
||||
: item.normalizedInput.size,
|
||||
existingSize: item.existingEntry?.size ?? 0,
|
||||
};
|
||||
});
|
||||
});
|
||||
sizeChangesByOwner.set(owner, sizeChanges);
|
||||
}
|
||||
|
||||
const storageAllowanceMax =
|
||||
storageAllowanceMaxOverride ?? preparedBatch.storageAllowanceMax;
|
||||
await this.#assertStorageAllowanceForBatch(
|
||||
preparedBatch.userId,
|
||||
sizeChanges,
|
||||
storageAllowanceMax,
|
||||
);
|
||||
for (const [owner, sizeChanges] of sizeChangesByOwner) {
|
||||
await this.#assertStorageAllowanceForBatch(
|
||||
owner,
|
||||
sizeChanges,
|
||||
storageAllowanceMax,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async uploadPreparedBatchItem(
|
||||
@@ -1622,11 +1646,14 @@ export class FSService extends PuterService {
|
||||
const parentPath = pathPosix.dirname(normalizedInput.path);
|
||||
const [, { parentEntries, createdDirectoryEntries }] =
|
||||
await Promise.all([
|
||||
this.#assertStorageAllowance(
|
||||
userId,
|
||||
normalizedInput.size,
|
||||
existingSize,
|
||||
storageAllowanceMax,
|
||||
this.#storageOwnerOf(normalizedInput.path, userId).then(
|
||||
(owner) =>
|
||||
this.#assertStorageAllowance(
|
||||
owner,
|
||||
normalizedInput.size,
|
||||
existingSize,
|
||||
storageAllowanceMax,
|
||||
),
|
||||
),
|
||||
this.stores.fsEntry.resolveParentDirectoriesBatchWithCreated(
|
||||
userId,
|
||||
@@ -1846,15 +1873,21 @@ export class FSService extends PuterService {
|
||||
};
|
||||
});
|
||||
|
||||
const allowanceChecks: Array<{
|
||||
incomingSize: number;
|
||||
existingSize: number;
|
||||
}> = [];
|
||||
const allowanceChecksByOwner = new Map<
|
||||
number,
|
||||
Array<{ incomingSize: number; existingSize: number }>
|
||||
>();
|
||||
for (const item of resolvedFileItems) {
|
||||
allowanceChecks.push({
|
||||
const owner = await this.#storageOwnerOf(
|
||||
item.normalizedInput.path,
|
||||
userId,
|
||||
);
|
||||
const checks = allowanceChecksByOwner.get(owner) ?? [];
|
||||
checks.push({
|
||||
incomingSize: item.normalizedInput.size,
|
||||
existingSize: item.existingEntry?.size ?? 0,
|
||||
});
|
||||
allowanceChecksByOwner.set(owner, checks);
|
||||
}
|
||||
const [
|
||||
,
|
||||
@@ -1863,10 +1896,14 @@ export class FSService extends PuterService {
|
||||
createdDirectoryEntries: createdParentDirectoryEntries,
|
||||
},
|
||||
] = await Promise.all([
|
||||
this.#assertStorageAllowanceForBatch(
|
||||
userId,
|
||||
allowanceChecks,
|
||||
storageAllowanceMax,
|
||||
Promise.all(
|
||||
[...allowanceChecksByOwner].map(([owner, checks]) =>
|
||||
this.#assertStorageAllowanceForBatch(
|
||||
owner,
|
||||
checks,
|
||||
storageAllowanceMax,
|
||||
),
|
||||
),
|
||||
),
|
||||
this.stores.fsEntry.resolveParentDirectoriesBatchWithCreated(
|
||||
userId,
|
||||
@@ -2690,8 +2727,12 @@ export class FSService extends PuterService {
|
||||
normalizedInput.thumbnail = null;
|
||||
|
||||
const existingSize = existingEntry?.size ?? 0;
|
||||
await this.#assertStorageAllowance(
|
||||
const storageOwner = await this.#storageOwnerOf(
|
||||
normalizedInput.path,
|
||||
userId,
|
||||
);
|
||||
await this.#assertStorageAllowance(
|
||||
storageOwner,
|
||||
normalizedInput.size,
|
||||
existingSize,
|
||||
storageAllowanceMax,
|
||||
@@ -2728,7 +2769,7 @@ export class FSService extends PuterService {
|
||||
}
|
||||
if (uploadedSize > normalizedInput.size) {
|
||||
await this.#assertStorageAllowance(
|
||||
userId,
|
||||
storageOwner,
|
||||
uploadedSize,
|
||||
existingSize,
|
||||
storageAllowanceMax,
|
||||
@@ -3163,7 +3204,6 @@ export class FSService extends PuterService {
|
||||
let created: FSEntry;
|
||||
try {
|
||||
created = await this.stores.fsEntry.createNonFileEntry({
|
||||
userId,
|
||||
parent,
|
||||
name,
|
||||
kind: 'directory',
|
||||
@@ -3235,7 +3275,6 @@ export class FSService extends PuterService {
|
||||
});
|
||||
}
|
||||
const created = await this.stores.fsEntry.createNonFileEntry({
|
||||
userId,
|
||||
parent,
|
||||
name,
|
||||
kind: 'empty-file',
|
||||
@@ -3329,7 +3368,6 @@ export class FSService extends PuterService {
|
||||
}
|
||||
}
|
||||
const created = await this.stores.fsEntry.createNonFileEntry({
|
||||
userId,
|
||||
parent: input.parent,
|
||||
name,
|
||||
kind: 'shortcut',
|
||||
@@ -3341,13 +3379,6 @@ export class FSService extends PuterService {
|
||||
|
||||
// -- Mutation: remove / move / copy ---------------------------------
|
||||
|
||||
/**
|
||||
* Remove an entry. For directories, descendants are walked and removed
|
||||
* (both DB rows and S3 objects). Emits `fs.remove.node` per file so the
|
||||
* thumbnail extension (and any other listener) can clean up side state.
|
||||
*
|
||||
* Does NOT enforce ACL — caller (controller) performs the `write` check.
|
||||
*/
|
||||
/**
|
||||
* Delete, move, and rename all ask ACL for `fs:write`, which cannot tell
|
||||
* them apart from an ordinary write — so the delete class is enforced here
|
||||
@@ -3410,6 +3441,14 @@ export class FSService extends PuterService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an entry. For directories, descendants are walked and removed
|
||||
* (both DB rows and S3 objects). Emits `fs.remove.node` per file so the
|
||||
* thumbnail extension (and any other listener) can clean up side state.
|
||||
*
|
||||
* The caller checks `write` on the entry; the parent check that governs
|
||||
* restructuring is enforced here.
|
||||
*/
|
||||
async remove(
|
||||
userId: number,
|
||||
input: {
|
||||
@@ -3722,9 +3761,20 @@ export class FSService extends PuterService {
|
||||
this.#stripReservedMetadataKeys(input.newMetadata),
|
||||
);
|
||||
|
||||
// Moving your entry into another tree hands it over, bytes included,
|
||||
// so they have to fit the new owner's allowance.
|
||||
const newOwnerId = destinationParent.userId;
|
||||
if (newOwnerId !== source.userId) {
|
||||
await this.#assertStorageAllowance(
|
||||
newOwnerId,
|
||||
await this.#entryStorageSize(source),
|
||||
);
|
||||
}
|
||||
|
||||
const updated = await this.stores.fsEntry.updateEntry(source.uuid, {
|
||||
name,
|
||||
path: finalPath,
|
||||
userId: newOwnerId,
|
||||
parentId: destinationParent.id,
|
||||
parentUid: destinationParent.uuid,
|
||||
...(metadataPatch !== undefined ? { metadata: metadataPatch } : {}),
|
||||
@@ -3735,6 +3785,7 @@ export class FSService extends PuterService {
|
||||
source.userId,
|
||||
source.path,
|
||||
finalPath,
|
||||
newOwnerId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3802,7 +3853,7 @@ export class FSService extends PuterService {
|
||||
// the allowance as writing them. Check before the overwrite below
|
||||
// removes anything, and credit what that removal frees.
|
||||
await this.#assertStorageAllowance(
|
||||
userId,
|
||||
destinationParent.userId,
|
||||
await this.#entryStorageSize(source),
|
||||
collision && input.overwrite
|
||||
? await this.#entryStorageSize(collision)
|
||||
@@ -3851,7 +3902,6 @@ export class FSService extends PuterService {
|
||||
// 2) Walk descendants; for each, compute new path by swapping prefix
|
||||
// 3) Create a new row (files copy S3 object; dirs just insert)
|
||||
const newRoot = await this.stores.fsEntry.createNonFileEntry({
|
||||
userId,
|
||||
parent: destinationParent,
|
||||
name,
|
||||
kind: 'directory',
|
||||
@@ -3882,7 +3932,6 @@ export class FSService extends PuterService {
|
||||
}
|
||||
const copied = descendant.isDir
|
||||
? await this.stores.fsEntry.createNonFileEntry({
|
||||
userId,
|
||||
parent: newParent,
|
||||
name: descendant.name,
|
||||
kind: 'directory',
|
||||
@@ -3918,7 +3967,6 @@ export class FSService extends PuterService {
|
||||
): Promise<FSEntry> {
|
||||
if (source.isSymlink) {
|
||||
return this.stores.fsEntry.createNonFileEntry({
|
||||
userId,
|
||||
parent: destinationParent,
|
||||
name: newName,
|
||||
kind: 'symlink',
|
||||
@@ -3929,7 +3977,6 @@ export class FSService extends PuterService {
|
||||
}
|
||||
if (source.isShortcut) {
|
||||
return this.stores.fsEntry.createNonFileEntry({
|
||||
userId,
|
||||
parent: destinationParent,
|
||||
name: newName,
|
||||
kind: 'shortcut',
|
||||
@@ -3945,7 +3992,6 @@ export class FSService extends PuterService {
|
||||
// the source as another empty-file entry instead of touching S3.
|
||||
if (hasNoBackingS3Object(source)) {
|
||||
return this.stores.fsEntry.createNonFileEntry({
|
||||
userId,
|
||||
parent: destinationParent,
|
||||
name: newName,
|
||||
kind: 'empty-file',
|
||||
|
||||
@@ -697,7 +697,7 @@ export class FSEntryStore extends PuterStore {
|
||||
);
|
||||
insertRows.push(
|
||||
expectedUuid,
|
||||
userId,
|
||||
parentEntry ? parentEntry.userId : userId,
|
||||
parentEntry ? parentEntry.id : null,
|
||||
parentEntry ? parentEntry.uuid : null,
|
||||
pathPosix.basename(dirPath),
|
||||
@@ -848,7 +848,7 @@ export class FSEntryStore extends PuterStore {
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ${trueLiteral}, ?, ?, ?, ${falseLiteral}, 0)${this.clients.db.insertIgnoreSuffix()}`,
|
||||
[
|
||||
uuidv4(),
|
||||
userId,
|
||||
parentEntry ? parentEntry.userId : userId,
|
||||
parentEntry ? parentEntry.id : null,
|
||||
parentEntry ? parentEntry.uuid : null,
|
||||
dirName,
|
||||
@@ -1875,7 +1875,7 @@ export class FSEntryStore extends PuterStore {
|
||||
entry.input.uuid,
|
||||
entry.bucket,
|
||||
entry.bucketRegion,
|
||||
userId,
|
||||
parentEntry.userId,
|
||||
parentEntry.id,
|
||||
parentEntry.uuid,
|
||||
entry.input.associatedAppId ?? null,
|
||||
@@ -1948,9 +1948,11 @@ export class FSEntryStore extends PuterStore {
|
||||
const placeholders = insertUuidChunk
|
||||
.map(() => '?')
|
||||
.join(', ');
|
||||
// By uuid alone — the rows just written belong to the
|
||||
// parent's owner, not necessarily the acting user.
|
||||
const rows = (await this.clients.db.tryHardRead(
|
||||
`SELECT ${this.#selectFsentriesColumns()} FROM fsentries WHERE user_id = ? AND uuid IN (${placeholders})`,
|
||||
[userId, ...insertUuidChunk],
|
||||
`SELECT ${this.#selectFsentriesColumns()} FROM fsentries WHERE uuid IN (${placeholders})`,
|
||||
insertUuidChunk,
|
||||
)) as unknown as FSEntryRow[];
|
||||
|
||||
const insertedEntries = rows.map((row) =>
|
||||
@@ -2276,9 +2278,11 @@ export class FSEntryStore extends PuterStore {
|
||||
*
|
||||
* Returns the inserted entry with a refreshed row read. Throws 409 on a
|
||||
* unique-key collision (caller should pre-check and dedupe).
|
||||
*
|
||||
* The row belongs to whoever owns `parent`, not to whoever created it, so a
|
||||
* shared subtree keeps one owner throughout.
|
||||
*/
|
||||
async createNonFileEntry(input: {
|
||||
userId: number;
|
||||
parent: FSEntry;
|
||||
name: string;
|
||||
kind: 'directory' | 'shortcut' | 'symlink' | 'empty-file';
|
||||
@@ -2331,7 +2335,7 @@ export class FSEntryStore extends PuterStore {
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
uuid,
|
||||
input.userId,
|
||||
input.parent.userId,
|
||||
input.parent.id,
|
||||
input.parent.uuid,
|
||||
input.name,
|
||||
@@ -2776,6 +2780,7 @@ export class FSEntryStore extends PuterStore {
|
||||
patch: {
|
||||
name?: string;
|
||||
path?: string;
|
||||
userId?: number;
|
||||
parentId?: number | null;
|
||||
parentUid?: string | null;
|
||||
thumbnail?: string | null;
|
||||
@@ -2800,6 +2805,7 @@ export class FSEntryStore extends PuterStore {
|
||||
|
||||
if (patch.name !== undefined) push('name', patch.name);
|
||||
if (patch.path !== undefined) push('path', patch.path);
|
||||
if (patch.userId !== undefined) push('user_id', patch.userId);
|
||||
if (patch.parentId !== undefined) push('parent_id', patch.parentId);
|
||||
if (patch.parentUid !== undefined) push('parent_uid', patch.parentUid);
|
||||
if (patch.thumbnail !== undefined) push('thumbnail', patch.thumbnail);
|
||||
@@ -2947,11 +2953,13 @@ export class FSEntryStore extends PuterStore {
|
||||
|
||||
// Rewrites path column for every descendant of `oldPrefix` to use `newPrefix`.
|
||||
// Used by move/rename when a directory is relocated. Cache for affected
|
||||
// entries is invalidated coarsely afterwards by the caller.
|
||||
// entries is invalidated coarsely afterwards by the caller. Pass
|
||||
// `newUserId` when the subtree also changes hands.
|
||||
async updatePathPrefixForUser(
|
||||
userId: number,
|
||||
oldPrefix: string,
|
||||
newPrefix: string,
|
||||
newUserId?: number,
|
||||
): Promise<number> {
|
||||
const normalizedOld = this.#normalizePath(oldPrefix);
|
||||
const normalizedNew = this.#normalizePath(newPrefix);
|
||||
@@ -2973,12 +2981,21 @@ export class FSEntryStore extends PuterStore {
|
||||
postgres: '? || SUBSTR(path, ?)',
|
||||
otherwise: 'CONCAT(?, SUBSTR(path, ?))',
|
||||
});
|
||||
const reowning = newUserId !== undefined && newUserId !== userId;
|
||||
const result = await this.clients.db.write(
|
||||
`UPDATE fsentries
|
||||
SET path = ${rewrittenPath},
|
||||
${reowning ? 'user_id = ?,' : ''}
|
||||
modified = ?
|
||||
WHERE user_id = ? AND path LIKE ? ESCAPE '!'`,
|
||||
[normalizedNew, oldPrefixLen + 1, now, userId, likePattern],
|
||||
[
|
||||
normalizedNew,
|
||||
oldPrefixLen + 1,
|
||||
...(reowning ? [newUserId] : []),
|
||||
now,
|
||||
userId,
|
||||
likePattern,
|
||||
],
|
||||
);
|
||||
const affected = this.#affectedRows(result);
|
||||
return affected;
|
||||
|
||||
Reference in New Issue
Block a user