release: prepare 0629001 portal updates

This commit is contained in:
john
2026-06-29 22:20:04 +08:00
parent 18ea4f82fd
commit a40e340a41
84 changed files with 17516 additions and 2475 deletions
+146 -8
View File
@@ -3,6 +3,7 @@ import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import sharp from 'sharp';
import { createAssetService, validateUploadRequest } from './mindspace-assets.mjs';
import { DEFAULT_MAX_FILE_BYTES } from './mindspace.mjs';
@@ -184,6 +185,23 @@ test('validateUploadRequest allows 5MB uploads by default', () => {
);
});
test('validateUploadRequest keeps image uploads below the image-specific limit', () => {
assert.doesNotThrow(() =>
validateUploadRequest({
filename: 'cover.jpg',
sizeBytes: 4 * 1024 * 1024,
}),
);
assert.throws(
() =>
validateUploadRequest({
filename: 'cover.jpg',
sizeBytes: 4 * 1024 * 1024 + 1,
}),
(error) => error.code === 'file_too_large',
);
});
test('completeUpload marks safe files ready and blocks script payloads', async () => {
const storageRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-assets-'));
const state = {
@@ -251,10 +269,16 @@ test('completeUpload publishes public images to the public temp image library',
h5Root,
idFactory: () => `id-${++nextId}`,
});
const png = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=',
'base64',
);
const png = await sharp({
create: {
width: 24,
height: 24,
channels: 4,
background: { r: 10, g: 160, b: 220, alpha: 0.8 },
},
})
.png()
.toBuffer();
const upload = await service.createUpload('user-1', {
categoryId: 'cat-public',
@@ -265,16 +289,130 @@ test('completeUpload publishes public images to the public temp image library',
const asset = await service.completeUpload('user-1', upload.id);
assert.equal(asset.status, 'ready');
assert.match(asset.publicUrl, /^https:\/\/m\.tkmind\.cn\/MindSpace\/user-1\/public\/\.tmp-images\/id-2\.png$/);
assert.equal(state.versions[0].storage_key, 'users/user-1/public/.tmp-images/id-2.png');
assert.match(asset.publicUrl, /^http:\/\/localhost:20081\/unsafe\//);
assert.match(state.versions[0].storage_key, /^users\/user-1\/images\/\d{4}-\d{2}-\d{2}\/look-id2\.png$/);
await assert.doesNotReject(() =>
fs.stat(path.join(storageRoot, 'users/user-1/public/.tmp-images/id-2.png')),
fs.stat(path.join(storageRoot, state.versions[0].storage_key)),
);
await assert.doesNotReject(() =>
fs.stat(path.join(h5Root, 'MindSpace/user-1/public/.tmp-images/id-2.png')),
fs.stat(path.join(h5Root, 'MindSpace/user-1', state.assets[0].original_filename)),
);
});
test('completeUpload normalizes large images into a bounded master asset', async () => {
const storageRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-assets-'));
const state = {
categories: [
{ id: 'cat-public', user_id: 'user-1', space_id: 'space-1', category_code: 'public' },
],
spaces: [
{
id: 'space-1',
user_id: 'user-1',
quota_bytes: 60 * 1024 * 1024,
used_bytes: 0,
reserved_bytes: 0,
status: 'active',
},
],
uploads: [],
assets: [],
versions: [],
};
let nextId = 0;
const service = createAssetService(createMockPool(state), {
storageRoot,
idFactory: () => `id-${++nextId}`,
});
const original = await sharp({
create: {
width: 4200,
height: 2800,
channels: 3,
background: { r: 220, g: 120, b: 60 },
},
})
.jpeg({ quality: 98 })
.toBuffer();
const upload = await service.createUpload('user-1', {
categoryId: 'cat-public',
filename: 'camera-roll.jpg',
sizeBytes: original.length,
});
await service.writeUploadContent('user-1', upload.id, original);
const asset = await service.completeUpload('user-1', upload.id);
const storedPath = path.join(storageRoot, state.versions[0].storage_key);
const storedBuffer = await fs.readFile(storedPath);
const metadata = await sharp(storedBuffer).metadata();
assert.equal(asset.status, 'ready');
assert.equal(asset.mimeType, 'image/jpeg');
assert.ok(asset.sizeBytes <= original.length);
assert.ok((metadata.width ?? 0) <= 2560);
assert.ok((metadata.height ?? 0) <= 2560);
});
test('completeUpload gives same-name public images unique storage keys', async () => {
const storageRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-assets-'));
const h5Root = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-h5-'));
const state = {
categories: [
{ id: 'cat-public', user_id: 'user-1', space_id: 'space-1', category_code: 'public' },
],
spaces: [
{
id: 'space-1',
user_id: 'user-1',
quota_bytes: 20 * 1024 * 1024,
used_bytes: 0,
reserved_bytes: 0,
status: 'active',
},
],
uploads: [],
assets: [],
versions: [],
};
let nextId = 0;
const service = createAssetService(createMockPool(state), {
storageRoot,
h5Root,
idFactory: () => `id-${++nextId}`,
});
const image = await sharp({
create: {
width: 16,
height: 16,
channels: 3,
background: { r: 120, g: 120, b: 240 },
},
})
.jpeg()
.toBuffer();
const first = await service.createUpload('user-1', {
categoryId: 'cat-public',
filename: 'e20.jpg',
sizeBytes: image.length,
});
await service.writeUploadContent('user-1', first.id, image);
await service.completeUpload('user-1', first.id);
const second = await service.createUpload('user-1', {
categoryId: 'cat-public',
filename: 'e20.jpg',
sizeBytes: image.length,
});
await service.writeUploadContent('user-1', second.id, image);
await service.completeUpload('user-1', second.id);
assert.equal(state.versions.length, 2);
assert.notEqual(state.versions[0].storage_key, state.versions[1].storage_key);
assert.match(state.versions[0].storage_key, /\/e20-id2\.jpg$/);
assert.match(state.versions[1].storage_key, /\/e20-id5\.jpg$/);
});
test('createUpload rejects another users category id', async () => {
const state = {
categories: [