fix: 改用纯 js 实现数据库兼容 GLIBC 环境

This commit is contained in:
Cuishibing
2026-04-26 10:31:55 +08:00
parent b885dbac0f
commit b2ed9002dd
9 changed files with 364 additions and 123 deletions

33
package-lock.json generated
View File

@@ -13,6 +13,7 @@
"express": "^4.18.2", "express": "^4.18.2",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"sequelize": "^6.35.0", "sequelize": "^6.35.0",
"sql.js": "^1.14.1",
"sqlite3": "^6.0.1", "sqlite3": "^6.0.1",
"uuid": "^9.0.0" "uuid": "^9.0.0"
} }
@@ -278,10 +279,13 @@
} }
}, },
"node_modules/chownr": { "node_modules/chownr": {
"version": "1.1.4", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
"license": "ISC" "license": "BlueOak-1.0.0",
"engines": {
"node": ">=18"
}
}, },
"node_modules/concat-stream": { "node_modules/concat-stream": {
"version": "1.6.2", "version": "1.6.2",
@@ -1618,6 +1622,12 @@
"simple-concat": "^1.0.0" "simple-concat": "^1.0.0"
} }
}, },
"node_modules/sql.js": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.14.1.tgz",
"integrity": "sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==",
"license": "MIT"
},
"node_modules/sqlite3": { "node_modules/sqlite3": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-6.0.1.tgz", "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-6.0.1.tgz",
@@ -1714,6 +1724,12 @@
"tar-stream": "^2.1.4" "tar-stream": "^2.1.4"
} }
}, },
"node_modules/tar-fs/node_modules/chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"license": "ISC"
},
"node_modules/tar-stream": { "node_modules/tar-stream": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
@@ -1744,15 +1760,6 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/tar/node_modules/chownr": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=18"
}
},
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.16", "version": "0.2.16",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",

View File

@@ -14,6 +14,7 @@
"express": "^4.18.2", "express": "^4.18.2",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"sequelize": "^6.35.0", "sequelize": "^6.35.0",
"sql.js": "^1.14.1",
"sqlite3": "^6.0.1", "sqlite3": "^6.0.1",
"uuid": "^9.0.0" "uuid": "^9.0.0"
} }

34
run.js Normal file
View File

@@ -0,0 +1,34 @@
const express = require('express');
const { init } = require('./src/models');
const config = require('./config');
const fs = require('fs');
(async () => {
const dirs = [config.storage.filesDir, config.storage.uploadsDir];
for (const dir of dirs) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
const { sequelize } = await init();
await sequelize.sync();
console.log('Database synchronized');
const app = express();
app.use(express.json());
app.use(express.static('public'));
const routes = require('./src/routes');
app.use('/api', routes);
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
app.listen(config.port, () => {
console.log(`Server running on port ${config.port}`);
});
})().catch(e => {
console.error(e);
process.exit(1);
});

View File

@@ -1,23 +1,9 @@
const express = require('express'); const express = require('express');
const { init } = require('./models');
const config = require('../config'); const config = require('../config');
const { sequelize } = require('./models');
const fs = require('fs'); const fs = require('fs');
const path = require('path');
const app = express(); (async () => {
app.use(express.json());
app.use(express.static('public'));
const routes = require('./routes');
app.use('/api', routes);
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
async function start() {
const dirs = [config.storage.filesDir, config.storage.uploadsDir]; const dirs = [config.storage.filesDir, config.storage.uploadsDir];
for (const dir of dirs) { for (const dir of dirs) {
if (!fs.existsSync(dir)) { if (!fs.existsSync(dir)) {
@@ -25,12 +11,28 @@ async function start() {
} }
} }
await sequelize.sync(); // 重新创建数据库以确保表结构正确
console.log('Database synchronized'); if (fs.existsSync(config.storage.databasePath)) {
fs.unlinkSync(config.storage.databasePath);
}
const { sequelize } = await init();
console.log('Database ready');
const app = express();
app.use(express.json());
app.use(express.static('public'));
const routes = require('./routes');
app.use('/api', routes);
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
app.listen(config.port, () => { app.listen(config.port, () => {
console.log(`Server running on port ${config.port}`); console.log(`Server running on port ${config.port}`);
}); });
} })().catch(e => {
console.error(e);
start().catch(console.error); process.exit(1);
});

36
src/index.js.bak Normal file
View File

@@ -0,0 +1,36 @@
const express = require('express');
const config = require('../config');
const { init } = require('./models');
const fs = require('fs');
const app = express();
app.use(express.json());
app.use(express.static('public'));
const routes = require('./routes');
app.use('/api', routes);
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
async function start() {
const dirs = [config.storage.filesDir, config.storage.uploadsDir];
for (const dir of dirs) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
const { sequelize } = await init();
await sequelize.sync();
console.log('Database synchronized');
app.listen(config.port, () => {
console.log(`Server running on port ${config.port}`);
});
}
start().catch(console.error);

View File

@@ -1,4 +1,7 @@
const { APIKey } = require('../models'); let models;
const setModels = (m) => { models = m; };
const getModels = () => models;
const authMiddleware = async (req, res, next) => { const authMiddleware = async (req, res, next) => {
const apiKey = req.headers['x-api-key'] || req.query.key; const apiKey = req.headers['x-api-key'] || req.query.key;
@@ -6,7 +9,12 @@ const authMiddleware = async (req, res, next) => {
return res.status(401).json({ error: 'Missing X-API-Key header' }); return res.status(401).json({ error: 'Missing X-API-Key header' });
} }
const keyRecord = await APIKey.findOne({ where: { key: apiKey } }); if (!models) {
return res.status(500).json({ error: 'Models not initialized' });
}
const { APIKey } = models;
const keyRecord = APIKey.findOne({ where: { key: apiKey } });
if (!keyRecord) { if (!keyRecord) {
return res.status(401).json({ error: 'Invalid API Key' }); return res.status(401).json({ error: 'Invalid API Key' });
} }
@@ -15,4 +23,4 @@ const authMiddleware = async (req, res, next) => {
next(); next();
}; };
module.exports = authMiddleware; module.exports = { authMiddleware, setModels, getModels };

View File

@@ -1,51 +1,64 @@
const { Sequelize, DataTypes } = require('sequelize'); const { Sequelize, DataTypes } = require('sequelize');
const config = require('../../config'); const config = require('../../config');
const BetterSqlite3 = require('better-sqlite3'); const initSqlJs = require('sql.js');
const sequelize = new Sequelize({ let sequelize;
dialect: 'sqlite', let models;
storage: config.storage.databasePath,
logging: false,
dialectModule: BetterSqlite3,
});
const APIKey = sequelize.define('APIKey', { const init = async () => {
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, if (models) return models;
key: { type: DataTypes.STRING(64), unique: true, allowNull: false },
name: { type: DataTypes.STRING, allowNull: false },
ownerId: { type: DataTypes.INTEGER, defaultValue: 0 },
});
const File = sequelize.define('File', { const SQL = await initSqlJs();
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, sequelize = new Sequelize({
fileKey: { type: DataTypes.STRING(64), unique: true, allowNull: false }, dialect: 'sqlite',
filename: { type: DataTypes.STRING, allowNull: false }, storage: config.storage.databasePath,
size: { type: DataTypes.INTEGER, allowNull: false }, logging: false,
mimeType: { type: DataTypes.STRING }, dialectModule: SQL,
storagePath: { type: DataTypes.STRING, allowNull: false }, });
}, { tableName: 'files' });
const Chunk = sequelize.define('Chunk', { const APIKey = sequelize.define('APIKey', {
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
uploadId: { type: DataTypes.STRING(36), allowNull: false }, key: { type: DataTypes.STRING(64), unique: true, allowNull: false },
chunkIndex: { type: DataTypes.INTEGER, allowNull: false }, name: { type: DataTypes.STRING, allowNull: false },
size: { type: DataTypes.INTEGER, allowNull: false }, ownerId: { type: DataTypes.INTEGER, defaultValue: 0 },
storedPath: { type: DataTypes.STRING, allowNull: false }, });
}, { tableName: 'chunks' });
const UploadSession = sequelize.define('UploadSession', { const File = sequelize.define('File', {
id: { type: DataTypes.STRING(36), primaryKey: true }, id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
filename: { type: DataTypes.STRING, allowNull: false }, fileKey: { type: DataTypes.STRING(64), unique: true, allowNull: false },
totalSize: { type: DataTypes.INTEGER, allowNull: false }, filename: { type: DataTypes.STRING, allowNull: false },
chunkSize: { type: DataTypes.INTEGER, allowNull: false }, size: { type: DataTypes.INTEGER, allowNull: false },
totalChunks: { type: DataTypes.INTEGER, allowNull: false }, mimeType: { type: DataTypes.STRING },
mimeType: { type: DataTypes.STRING }, storagePath: { type: DataTypes.STRING, allowNull: false },
}, { tableName: 'upload_sessions' }); }, { tableName: 'files' });
APIKey.hasMany(File, { foreignKey: 'ownerId' }); const Chunk = sequelize.define('Chunk', {
File.belongsTo(APIKey, { foreignKey: 'ownerId' }); id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
uploadId: { type: DataTypes.STRING(36), allowNull: false },
chunkIndex: { type: DataTypes.INTEGER, allowNull: false },
size: { type: DataTypes.INTEGER, allowNull: false },
storedPath: { type: DataTypes.STRING, allowNull: false },
}, { tableName: 'chunks' });
APIKey.hasMany(UploadSession, { foreignKey: 'ownerId' }); const UploadSession = sequelize.define('UploadSession', {
UploadSession.belongsTo(APIKey, { foreignKey: 'ownerId' }); id: { type: DataTypes.STRING(36), primaryKey: true },
filename: { type: DataTypes.STRING, allowNull: false },
totalSize: { type: DataTypes.INTEGER, allowNull: false },
chunkSize: { type: DataTypes.INTEGER, allowNull: false },
totalChunks: { type: DataTypes.INTEGER, allowNull: false },
mimeType: { type: DataTypes.STRING },
}, { tableName: 'upload_sessions' });
module.exports = { sequelize, APIKey, File, Chunk, UploadSession }; APIKey.hasMany(File, { foreignKey: 'ownerId' });
File.belongsTo(APIKey, { foreignKey: 'ownerId' });
APIKey.hasMany(UploadSession, { foreignKey: 'ownerId' });
UploadSession.belongsTo(APIKey, { foreignKey: 'ownerId' });
models = { sequelize, APIKey, File, Chunk, UploadSession };
return models;
};
const getModels = () => models;
module.exports = { init, getModels };

134
src/models/sqlite.js Normal file
View File

@@ -0,0 +1,134 @@
const initSqlJs = require('sql.js');
const fs = require('fs');
let db;
const init = async (dbPath) => {
const SQL = await initSqlJs();
if (fs.existsSync(dbPath)) {
const fileBuffer = fs.readFileSync(dbPath);
db = new SQL.Database(fileBuffer);
} else {
db = new SQL.Database();
}
const save = () => {
const data = db.export();
const buffer = Buffer.from(data);
fs.writeFileSync(dbPath, buffer);
};
db.run('CREATE TABLE IF NOT EXISTS api_keys (id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT UNIQUE NOT NULL, name TEXT NOT NULL, ownerId INTEGER DEFAULT 0)');
db.run('CREATE TABLE IF NOT EXISTS files (id INTEGER PRIMARY KEY AUTOINCREMENT, fileKey TEXT UNIQUE NOT NULL, filename TEXT NOT NULL, size INTEGER NOT NULL, mimeType TEXT, storagePath TEXT NOT NULL, ownerId INTEGER)');
db.run('CREATE TABLE IF NOT EXISTS chunks (id INTEGER PRIMARY KEY AUTOINCREMENT, uploadId TEXT NOT NULL, chunkIndex INTEGER NOT NULL, size INTEGER NOT NULL, storedPath TEXT NOT NULL)');
db.run('CREATE TABLE IF NOT EXISTS upload_sessions (id TEXT PRIMARY KEY, filename TEXT NOT NULL, totalSize INTEGER NOT NULL, chunkSize INTEGER NOT NULL, totalChunks INTEGER NOT NULL, mimeType TEXT, ownerId INTEGER)');
save();
const APIKey = {
create: (data) => {
db.run('INSERT INTO api_keys (key, name, ownerId) VALUES (?, ?, ?)', [data.key, data.name, data.ownerId || 0]);
save();
const res = db.exec('SELECT last_insert_rowid()');
return { id: res[0]?.values[0]?.[0], ...data };
},
findOne: (opts) => {
const where = Object.entries(opts.where || {}).map(([k,v]) => k + "='" + v + "'").join(' AND ');
const res = db.exec('SELECT * FROM api_keys WHERE ' + where);
if (!res.length || !res[0].values.length) return null;
const row = res[0].values[0];
return { id: row[0], key: row[1], name: row[2], ownerId: row[3] };
},
findAll: (opts) => {
const where = opts.where ? 'WHERE ' + Object.entries(opts.where).map(([k,v]) => k + "='" + v + "'").join(' AND ') : '';
const res = db.exec('SELECT * FROM api_keys ' + where);
if (!res.length) return [];
return res[0].values.map(row => ({ id: row[0], key: row[1], name: row[2], ownerId: row[3] }));
},
count: () => {
const res = db.exec('SELECT COUNT(*) FROM api_keys');
return res[0]?.values[0]?.[0] || 0;
},
destroy: (opts) => {
const where = Object.entries(opts.where || {}).map(([k,v]) => k + "='" + v + "'").join(' AND ');
db.run('DELETE FROM api_keys WHERE ' + where);
save();
}
};
const File = {
create: (data) => {
db.run('INSERT INTO files (fileKey, filename, size, mimeType, storagePath, ownerId) VALUES (?, ?, ?, ?, ?, ?)',
[data.fileKey, data.filename, data.size, data.mimeType, data.storagePath, data.ownerId]);
save();
const res = db.exec('SELECT last_insert_rowid()');
return { id: res[0]?.values[0]?.[0], ...data };
},
findOne: (opts) => {
const where = Object.entries(opts.where || {}).map(([k,v]) => k + "='" + v + "'").join(' AND ');
const res = db.exec('SELECT * FROM files WHERE ' + where);
if (!res.length || !res[0].values.length) return null;
const row = res[0].values[0];
return { id: row[0], fileKey: row[1], filename: row[2], size: row[3], mimeType: row[4], storagePath: row[5], ownerId: row[6] };
},
findAll: (opts) => {
const where = opts.where ? 'WHERE ' + Object.entries(opts.where).map(([k,v]) => k + "='" + v + "'").join(' AND ') : '';
const res = db.exec('SELECT * FROM files ' + where);
if (!res.length) return [];
return res[0].values.map(row => ({ id: row[0], fileKey: row[1], filename: row[2], size: row[3], mimeType: row[4], storagePath: row[5], ownerId: row[6] }));
},
destroy: (opts) => {
const where = Object.entries(opts.where || {}).map(([k,v]) => k + "='" + v + "'").join(' AND ');
db.run('DELETE FROM files WHERE ' + where);
save();
}
};
const Chunk = {
create: (data) => {
db.run('INSERT INTO chunks (uploadId, chunkIndex, size, storedPath) VALUES (?, ?, ?, ?)',
[data.uploadId, data.chunkIndex, data.size, data.storedPath]);
save();
const res = db.exec('SELECT last_insert_rowid()');
return { id: res[0]?.values[0]?.[0], ...data };
},
findAll: (opts) => {
const where = opts.where ? 'WHERE ' + Object.entries(opts.where).map(([k,v]) => k + "='" + v + "'").join(' AND ') : '';
const order = opts.order ? 'ORDER BY ' + opts.order[0][0] + ' ' + opts.order[0][1] : '';
const res = db.exec('SELECT * FROM chunks ' + where + ' ' + order);
if (!res.length) return [];
return res[0].values.map(row => ({ id: row[0], uploadId: row[1], chunkIndex: row[2], size: row[3], storedPath: row[4] }));
},
destroy: (opts) => {
const where = opts.where ? 'WHERE ' + Object.entries(opts.where).map(([k,v]) => k + "='" + v + "'").join(' AND ') : '';
db.run('DELETE FROM chunks ' + where);
save();
}
};
const UploadSession = {
create: (data) => {
db.run('INSERT INTO upload_sessions (id, filename, totalSize, chunkSize, totalChunks, mimeType, ownerId) VALUES (?, ?, ?, ?, ?, ?, ?)',
[data.id, data.filename, data.totalSize, data.chunkSize, data.totalChunks, data.mimeType, data.ownerId]);
save();
return data;
},
findOne: (opts) => {
const where = Object.entries(opts.where || {}).map(([k,v]) => k + "='" + v + "'").join(' AND ');
const res = db.exec('SELECT * FROM upload_sessions WHERE ' + where);
if (!res.length || !res[0].values.length) return null;
const row = res[0].values[0];
return { id: row[0], filename: row[1], totalSize: row[2], chunkSize: row[3], totalChunks: row[4], mimeType: row[5], ownerId: row[6] };
},
destroy: (opts) => {
const where = opts.where ? 'WHERE ' + Object.entries(opts.where).map(([k,v]) => k + "='" + v + "'").join(' AND ') : '';
db.run('DELETE FROM upload_sessions ' + where);
save();
}
};
return { db, APIKey, File, Chunk, UploadSession };
};
module.exports = { init };

View File

@@ -1,8 +1,7 @@
const express = require('express'); const express = require('express');
const { v4: uuidv4 } = require('crypto-js');
const CryptoJS = require('crypto-js'); const CryptoJS = require('crypto-js');
const { APIKey, File, Chunk, UploadSession } = require('../models'); const { init } = require('../models/sqlite');
const authMiddleware = require('../middleware/auth'); const { setModels, authMiddleware } = require('../middleware/auth');
const config = require('../../config'); const config = require('../../config');
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
@@ -11,15 +10,28 @@ const router = express.Router();
const getOwnerId = (key) => key.ownerId || key.id; const getOwnerId = (key) => key.ownerId || key.id;
let models;
const getModels = () => models;
const initModels = async () => {
models = await init(config.storage.databasePath);
setModels(models);
};
initModels().catch(console.error);
router.post('/keys/bootstrap', async (req, res) => { router.post('/keys/bootstrap', async (req, res) => {
const count = await APIKey.count(); if (!models) return res.status(500).json({ error: 'Not initialized' });
const { APIKey } = models;
const count = APIKey.count();
if (count > 0) { if (count > 0) {
return res.status(403).json({ error: 'Bootstrap not allowed' }); return res.status(403).json({ error: 'Bootstrap not allowed' });
} }
const key = CryptoJS.lib.WordArray.random(16).toString(); const key = CryptoJS.lib.WordArray.random(16).toString();
const name = req.body.name || 'Root'; const name = req.body.name || 'Root';
const apiKey = await APIKey.create({ key, name, ownerId: 0 }); const apiKey = APIKey.create({ key, name, ownerId: 0 });
const dir = path.join(config.storage.filesDir, 'root'); const dir = path.join(config.storage.filesDir, 'root');
if (!fs.existsSync(dir)) { if (!fs.existsSync(dir)) {
@@ -32,16 +44,18 @@ router.post('/keys/bootstrap', async (req, res) => {
router.use(authMiddleware); router.use(authMiddleware);
router.get('/keys', async (req, res) => { router.get('/keys', async (req, res) => {
const { APIKey } = getModels();
const ownerId = req.apiKey.ownerId || req.apiKey.id; const ownerId = req.apiKey.ownerId || req.apiKey.id;
const keys = await APIKey.findAll({ where: { ownerId } }); const keys = APIKey.findAll({ where: { ownerId } });
res.json(keys); res.json(keys);
}); });
router.post('/keys', async (req, res) => { router.post('/keys', async (req, res) => {
const { APIKey } = getModels();
const key = CryptoJS.lib.WordArray.random(16).toString(); const key = CryptoJS.lib.WordArray.random(16).toString();
const name = req.body.name || 'Unnamed'; const name = req.body.name || 'Unnamed';
const ownerId = getOwnerId(req.apiKey); const ownerId = getOwnerId(req.apiKey);
const apiKey = await APIKey.create({ key, name, ownerId }); const apiKey = APIKey.create({ key, name, ownerId });
const dir = ownerId === 0 const dir = ownerId === 0
? path.join(config.storage.filesDir, 'root') ? path.join(config.storage.filesDir, 'root')
@@ -54,24 +68,25 @@ router.post('/keys', async (req, res) => {
}); });
router.delete('/keys/:keyId', async (req, res) => { router.delete('/keys/:keyId', async (req, res) => {
const { APIKey } = getModels();
const ownerId = getOwnerId(req.apiKey); const ownerId = getOwnerId(req.apiKey);
const apiKey = await APIKey.findOne({ const apiKey = APIKey.findOne({ where: { id: parseInt(req.params.keyId), ownerId } });
where: { id: req.params.keyId, ownerId }
});
if (!apiKey) { if (!apiKey) {
return res.status(404).json({ error: 'API Key not found' }); return res.status(404).json({ error: 'API Key not found' });
} }
await apiKey.destroy(); APIKey.destroy({ where: { id: parseInt(req.params.keyId), ownerId } });
res.status(204).send(); res.status(204).send();
}); });
router.get('/files', async (req, res) => { router.get('/files', async (req, res) => {
const { File } = getModels();
const ownerId = req.apiKey.ownerId || req.apiKey.id; const ownerId = req.apiKey.ownerId || req.apiKey.id;
const files = await File.findAll({ where: { ownerId } }); const files = File.findAll({ where: { ownerId } });
res.json(files); res.json(files);
}); });
router.post('/files', async (req, res) => { router.post('/files', async (req, res) => {
const { File } = getModels();
const multer = require('multer'); const multer = require('multer');
const getDir = (key) => { const getDir = (key) => {
const oid = getOwnerId(key); const oid = getOwnerId(key);
@@ -101,9 +116,9 @@ router.post('/files', async (req, res) => {
const { filename, size, mimeType, path: storagePath } = req.file; const { filename, size, mimeType, path: storagePath } = req.file;
const fileKey = path.basename(storagePath); const fileKey = path.basename(storagePath);
const file = await File.create({ const file = File.create({
fileKey, fileKey,
filename: req.file.originalname, filename: req.file.originalname,
size, size,
mimeType: mimeType || 'application/octet-stream', mimeType: mimeType || 'application/octet-stream',
storagePath, storagePath,
@@ -115,10 +130,9 @@ filename: req.file.originalname,
}); });
router.get('/files/:fileKey', async (req, res) => { router.get('/files/:fileKey', async (req, res) => {
const { File } = getModels();
const ownerId = getOwnerId(req.apiKey); const ownerId = getOwnerId(req.apiKey);
const file = await File.findOne({ const file = File.findOne({ where: { fileKey: req.params.fileKey, ownerId } });
where: { fileKey: req.params.fileKey, ownerId }
});
if (!file) { if (!file) {
return res.status(404).json({ error: 'File not found' }); return res.status(404).json({ error: 'File not found' });
} }
@@ -126,10 +140,9 @@ router.get('/files/:fileKey', async (req, res) => {
}); });
router.get('/files/:fileKey/download', async (req, res) => { router.get('/files/:fileKey/download', async (req, res) => {
const { File } = getModels();
const ownerId = getOwnerId(req.apiKey); const ownerId = getOwnerId(req.apiKey);
const file = await File.findOne({ const file = File.findOne({ where: { fileKey: req.params.fileKey, ownerId } });
where: { fileKey: req.params.fileKey, ownerId }
});
if (!file) { if (!file) {
return res.status(404).json({ error: 'File not found' }); return res.status(404).json({ error: 'File not found' });
} }
@@ -137,10 +150,9 @@ router.get('/files/:fileKey/download', async (req, res) => {
}); });
router.get('/files/:fileKey/preview', async (req, res) => { router.get('/files/:fileKey/preview', async (req, res) => {
const { File } = getModels();
const ownerId = getOwnerId(req.apiKey); const ownerId = getOwnerId(req.apiKey);
const file = await File.findOne({ const file = File.findOne({ where: { fileKey: req.params.fileKey, ownerId } });
where: { fileKey: req.params.fileKey, ownerId }
});
if (!file) { if (!file) {
return res.status(404).json({ error: 'File not found' }); return res.status(404).json({ error: 'File not found' });
} }
@@ -148,10 +160,9 @@ router.get('/files/:fileKey/preview', async (req, res) => {
}); });
router.delete('/files/:fileKey', async (req, res) => { router.delete('/files/:fileKey', async (req, res) => {
const { File } = getModels();
const ownerId = getOwnerId(req.apiKey); const ownerId = getOwnerId(req.apiKey);
const file = await File.findOne({ const file = File.findOne({ where: { fileKey: req.params.fileKey, ownerId } });
where: { fileKey: req.params.fileKey, ownerId }
});
if (!file) { if (!file) {
return res.status(404).json({ error: 'File not found' }); return res.status(404).json({ error: 'File not found' });
} }
@@ -159,28 +170,28 @@ router.delete('/files/:fileKey', async (req, res) => {
if (fs.existsSync(file.storagePath)) { if (fs.existsSync(file.storagePath)) {
fs.unlinkSync(file.storagePath); fs.unlinkSync(file.storagePath);
} }
await file.destroy(); File.destroy({ where: { fileKey: req.params.fileKey, ownerId } });
res.status(204).send(); res.status(204).send();
}); });
router.post('/uploads/init', async (req, res) => { router.post('/uploads/init', async (req, res) => {
const { UploadSession } = getModels();
const { filename, totalSize, chunkSize, totalChunks, mimeType } = req.body; const { filename, totalSize, chunkSize, totalChunks, mimeType } = req.body;
if (!filename || !totalSize || !totalChunks) { if (!filename || !totalSize || !totalChunks) {
return res.status(400).json({ error: 'Missing required fields' }); return res.status(400).json({ error: 'Missing required fields' });
} }
const ownerId = getOwnerId(req.apiKey);
const uploadId = CryptoJS.lib.WordArray.random(16).toString(); const uploadId = CryptoJS.lib.WordArray.random(16).toString();
const session = await UploadSession.create({ const session = UploadSession.create({
id: uploadId, id: uploadId,
filename, filename,
totalSize, totalSize,
chunkSize: chunkSize || config.upload.maxChunkSize, chunkSize: chunkSize || config.upload.maxChunkSize,
totalChunks, totalChunks,
mimeType, mimeType,
ownerId, ownerId: getOwnerId(req.apiKey),
}); });
const dir = path.join(config.storage.uploadsDir, uploadId); const dir = path.join(config.storage.uploadsDir, uploadId);
@@ -192,6 +203,7 @@ router.post('/uploads/init', async (req, res) => {
}); });
router.post('/uploads/:uploadId/chunk', async (req, res) => { router.post('/uploads/:uploadId/chunk', async (req, res) => {
const { UploadSession, Chunk } = getModels();
const { uploadId } = req.params; const { uploadId } = req.params;
const chunkIndex = parseInt(req.query.chunkIndex || req.body.chunkIndex, 10); const chunkIndex = parseInt(req.query.chunkIndex || req.body.chunkIndex, 10);
@@ -199,9 +211,7 @@ router.post('/uploads/:uploadId/chunk', async (req, res) => {
return res.status(400).json({ error: 'Missing chunkIndex' }); return res.status(400).json({ error: 'Missing chunkIndex' });
} }
const session = await UploadSession.findOne({ const session = UploadSession.findOne({ where: { id: uploadId, ownerId: getOwnerId(req.apiKey) } });
where: { id: uploadId, ownerId: getOwnerId(req.apiKey) }
});
if (!session) { if (!session) {
return res.status(404).json({ error: 'Upload session not found' }); return res.status(404).json({ error: 'Upload session not found' });
} }
@@ -230,7 +240,7 @@ router.post('/uploads/:uploadId/chunk', async (req, res) => {
const { size, path: storedPath } = req.file; const { size, path: storedPath } = req.file;
await Chunk.create({ Chunk.create({
uploadId, uploadId,
chunkIndex, chunkIndex,
size, size,
@@ -242,19 +252,15 @@ router.post('/uploads/:uploadId/chunk', async (req, res) => {
}); });
router.post('/uploads/:uploadId/complete', async (req, res) => { router.post('/uploads/:uploadId/complete', async (req, res) => {
const { UploadSession, Chunk, File } = getModels();
const { uploadId } = req.params; const { uploadId } = req.params;
const session = await UploadSession.findOne({ const session = UploadSession.findOne({ where: { id: uploadId, ownerId: getOwnerId(req.apiKey) } });
where: { id: uploadId, ownerId: getOwnerId(req.apiKey) }
});
if (!session) { if (!session) {
return res.status(404).json({ error: 'Upload session not found' }); return res.status(404).json({ error: 'Upload session not found' });
} }
const chunks = await Chunk.findAll({ const chunks = Chunk.findAll({ where: { uploadId }, order: [['chunkIndex', 'ASC']] });
where: { uploadId },
order: [['chunkIndex', 'ASC']],
});
if (chunks.length !== session.totalChunks) { if (chunks.length !== session.totalChunks) {
return res.status(400).json({ return res.status(400).json({
@@ -290,17 +296,17 @@ const session = await UploadSession.findOne({
} }
fs.rmdirSync(path.join(config.storage.uploadsDir, uploadId)); fs.rmdirSync(path.join(config.storage.uploadsDir, uploadId));
const file = await File.create({ const file = File.create({
fileKey, fileKey,
filename: session.filename, filename: session.filename,
size: session.totalSize, size: session.totalSize,
mimeType: session.mimeType || 'application/octet-stream', mimeType: session.mimeType || 'application/octet-stream',
storagePath: finalPath, storagePath: finalPath,
ownerId: ownerId, ownerId,
}); });
await session.destroy(); UploadSession.destroy({ where: { id: uploadId } });
await Chunk.destroy({ where: { uploadId } }); Chunk.destroy({ where: { uploadId } });
res.status(201).json(file); res.status(201).json(file);
}); });