- 实现用户登录、登出、密码修改等认证功能 - 添加会话管理和权限控制中间件 - 创建图片批注组件和相关API路由 - 实现批注的增删改查功能 - 添加Docker和Git忽略配置文件 - 创建系统架构文档和开发约定说明 - 集成认证模块到前端应用路由中
91 lines
3.7 KiB
TypeScript
91 lines
3.7 KiB
TypeScript
import COS from 'cos-nodejs-sdk-v5';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { randomUUID } from 'crypto';
|
|
import { database } from './database.js';
|
|
import { decryptSecret } from './configCrypto.js';
|
|
|
|
export interface StorageConfigRecord {
|
|
id: number;
|
|
region: string;
|
|
bucket: string;
|
|
public_base_url: string;
|
|
cdn_domain: string;
|
|
path_prefix: string;
|
|
secret_id_encrypted: string;
|
|
secret_key_encrypted: string;
|
|
}
|
|
|
|
export interface StoredUpload {
|
|
url: string;
|
|
storageProvider: 'local' | 'tencent_cos';
|
|
storageKey: string;
|
|
}
|
|
|
|
export async function getActiveStorageConfig(): Promise<StorageConfigRecord | undefined> {
|
|
return database.one<StorageConfigRecord>(`
|
|
SELECT id, region, bucket, public_base_url, cdn_domain, path_prefix,
|
|
secret_id_encrypted, secret_key_encrypted
|
|
FROM storage_configs WHERE status = 'active' ORDER BY id DESC LIMIT 1
|
|
`);
|
|
}
|
|
|
|
function createClient(config: StorageConfigRecord) {
|
|
return new COS({
|
|
SecretId: decryptSecret(config.secret_id_encrypted),
|
|
SecretKey: decryptSecret(config.secret_key_encrypted),
|
|
});
|
|
}
|
|
|
|
function cleanPrefix(value: string): string {
|
|
return value.trim().replace(/^\/+|\/+$/g, '').replace(/\/{2,}/g, '/');
|
|
}
|
|
|
|
function objectUrl(config: StorageConfigRecord, key: string): string {
|
|
const base = (config.cdn_domain || config.public_base_url || `https://${config.bucket}.cos.${config.region}.myqcloud.com`).replace(/\/+$/, '');
|
|
return `${base}/${key.split('/').map(encodeURIComponent).join('/')}`;
|
|
}
|
|
|
|
function safeError(error: unknown): string {
|
|
if (!error || typeof error !== 'object') return 'COS 连接失败';
|
|
const item = error as { code?: string; statusCode?: number; message?: string };
|
|
return [item.code, item.statusCode, item.message].filter(Boolean).join(' · ').slice(0, 300) || 'COS 连接失败';
|
|
}
|
|
|
|
export async function testStorageConfig(config: StorageConfigRecord): Promise<string> {
|
|
const cos = createClient(config);
|
|
const prefix = cleanPrefix(config.path_prefix);
|
|
const key = `${prefix ? `${prefix}/` : ''}.delivery-desk-check/${randomUUID()}.txt`;
|
|
try {
|
|
await cos.putObject({ Bucket: config.bucket, Region: config.region, Key: key, Body: Buffer.from('delivery-desk storage check'), ContentType: 'text/plain' });
|
|
await cos.getObject({ Bucket: config.bucket, Region: config.region, Key: key });
|
|
await cos.deleteObject({ Bucket: config.bucket, Region: config.region, Key: key });
|
|
return '上传、读取和删除测试均已通过';
|
|
} catch (error) {
|
|
try { await cos.deleteObject({ Bucket: config.bucket, Region: config.region, Key: key }); } catch { /* best-effort cleanup */ }
|
|
throw new Error(safeError(error));
|
|
}
|
|
}
|
|
|
|
export async function storeUploadedFile(file: { filename: string; originalname?: string; mimetype?: string; path: string }): Promise<StoredUpload> {
|
|
const config = await getActiveStorageConfig();
|
|
if (!config) return { url: file.filename, storageProvider: 'local', storageKey: file.filename };
|
|
|
|
const prefix = cleanPrefix(config.path_prefix);
|
|
const now = new Date();
|
|
const extension = path.extname(file.originalname || file.filename).toLowerCase() || '.jpg';
|
|
const month = `${now.getUTCFullYear()}/${String(now.getUTCMonth() + 1).padStart(2, '0')}`;
|
|
const key = `${prefix ? `${prefix}/` : ''}originals/${month}/${randomUUID()}${extension}`;
|
|
const cos = createClient(config);
|
|
await cos.putObject({
|
|
Bucket: config.bucket,
|
|
Region: config.region,
|
|
Key: key,
|
|
Body: fs.createReadStream(file.path),
|
|
ContentLength: fs.statSync(file.path).size,
|
|
ContentType: file.mimetype || 'application/octet-stream',
|
|
});
|
|
fs.unlinkSync(file.path);
|
|
return { url: objectUrl(config, key), storageProvider: 'tencent_cos', storageKey: key };
|
|
}
|