Compare commits

...

5 Commits

Author SHA1 Message Date
Cuishibing
dd5d2fcd70 feat: 添加停止脚本 2026-04-26 14:47:31 +08:00
Cuishibing
384e437440 feat: 添加启动脚本支持端口、存储目录、密码参数 2026-04-26 13:00:22 +08:00
Cuishibing
f8fbd290c7 fix: 管理页面添加密码输入提示 2026-04-26 12:55:28 +08:00
Cuishibing
e53a674bff feat: 添加管理员密码保护 bootstrap 接口 2026-04-26 10:53:16 +08:00
Cuishibing
b2ed9002dd fix: 改用纯 js 实现数据库兼容 GLIBC 环境 2026-04-26 10:31:55 +08:00
14 changed files with 463 additions and 139 deletions

View File

@@ -12,15 +12,16 @@ npm install
npm start # 默认 3000 端口, ./storage 目录 npm start # 默认 3000 端口, ./storage 目录
PORT=8080 npm start # 指定端口 PORT=8080 npm start # 指定端口
STORAGE_DIR=/data myoss # 指定存储目录 STORAGE_DIR=/data myoss # 指定存储目录
PORT=8080 STORAGE_DIR=/data myoss # 同时指定
# 首次启动需要设置管理员密码
ADMIN_PASSWORD=yourpassword npm start
``` ```
## 使用方法 ### 初始化首个 API Key
### 1. 创建首个 API Keybootstrap
```bash ```bash
curl -X POST http://localhost:3000/api/keys/bootstrap -H "Content-Type: application/json" -d '{"name":"root"}' curl -X POST http://localhost:3000/api/keys/bootstrap \
# 返回: {"key":"xxx","name":"root"} -H "Content-Type: application/json" \
-d '{"password":"yourpassword","name":"root"}'
``` ```
### 2. 上传文件 ### 2. 上传文件

View File

@@ -1,5 +1,8 @@
module.exports = { module.exports = {
port: process.env.PORT || 3000, port: process.env.PORT || 3000,
admin: {
password: process.env.ADMIN_PASSWORD || '',
},
storage: { storage: {
baseDir: process.env.STORAGE_DIR || './storage', baseDir: process.env.STORAGE_DIR || './storage',
get filesDir() { return this.baseDir + '/files'; }, get filesDir() { return this.baseDir + '/files'; },

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"
} }

View File

@@ -100,7 +100,6 @@
} }
async function setup() { async function setup() {
// 检查是否已有 key
const savedKey = localStorage.getItem('myoss_api_key'); const savedKey = localStorage.getItem('myoss_api_key');
if (savedKey) { if (savedKey) {
apiKey = savedKey; apiKey = savedKey;
@@ -108,20 +107,19 @@
return; return;
} }
const name = document.getElementById('setup-name').value; // 检查是否已有 key
const password = prompt('请输入管理员密码:');
if (!password) return;
const name = document.getElementById('setup-name').value || 'root';
const res = await fetch('/api/keys/bootstrap', { const res = await fetch('/api/keys/bootstrap', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }) body: JSON.stringify({ password, name })
}); });
const data = await res.json(); const data = await res.json();
if (data.error) { if (data.error) {
// 已初始化过,提示用户输入已有 key alert(data.error);
const key = prompt('服务已初始化,请输入已有的 API Key:');
if (key) {
localStorage.setItem('myoss_api_key', key);
location.reload();
}
return; return;
} }
apiKey = data.key; apiKey = data.key;

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,12 +1,19 @@
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;
let models;
const init = async () => {
if (models) return models;
const SQL = await initSqlJs();
sequelize = new Sequelize({
dialect: 'sqlite', dialect: 'sqlite',
storage: config.storage.databasePath, storage: config.storage.databasePath,
logging: false, logging: false,
dialectModule: BetterSqlite3, dialectModule: SQL,
}); });
const APIKey = sequelize.define('APIKey', { const APIKey = sequelize.define('APIKey', {
@@ -48,4 +55,10 @@ File.belongsTo(APIKey, { foreignKey: 'ownerId' });
APIKey.hasMany(UploadSession, { foreignKey: 'ownerId' }); APIKey.hasMany(UploadSession, { foreignKey: 'ownerId' });
UploadSession.belongsTo(APIKey, { foreignKey: 'ownerId' }); UploadSession.belongsTo(APIKey, { foreignKey: 'ownerId' });
module.exports = { sequelize, APIKey, File, Chunk, UploadSession }; 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,32 @@ 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(); const { APIKey } = getModels();
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 { password, name } = req.body;
if (!config.admin.password || password !== config.admin.password) {
return res.status(401).json({ error: 'Invalid admin password' });
}
const key = CryptoJS.lib.WordArray.random(16).toString(); const key = CryptoJS.lib.WordArray.random(16).toString();
const name = req.body.name || 'Root'; const keyName = name || 'Root';
const apiKey = await APIKey.create({ key, name, ownerId: 0 }); const apiKey = APIKey.create({ key, name: keyName, 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 +48,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 +72,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,7 +120,7 @@ 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,
@@ -115,10 +134,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 +144,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 +154,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 +164,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 +174,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 +207,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 +215,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 +244,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 +256,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 +300,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);
}); });

65
start.sh Executable file
View File

@@ -0,0 +1,65 @@
#!/bin/bash
PORT=3000
STORAGE_DIR=""
ADMIN_PASSWORD=""
show_help() {
echo "Usage: $0 [options]"
echo ""
echo "Options:"
echo " -p, --port PORT 服务端口 (默认: 3000)"
echo " -s, --storage DIR 存储目录 (默认: ./storage)"
echo " -w, --password PASS 管理员密码"
echo " -h, --help 显示帮助"
}
while [[ $# -gt 0 ]]; do
case $1 in
-p|--port)
PORT="$2"
shift 2
;;
-s|--storage)
STORAGE_DIR="$2"
shift 2
;;
-w|--password)
ADMIN_PASSWORD="$2"
shift 2
;;
-h|--help)
show_help
exit 0
;;
*)
echo "未知参数: $1"
show_help
exit 1
;;
esac
done
# 构建命令
CMD="npm start"
ENV_VARS="PORT=$PORT"
if [ -n "$STORAGE_DIR" ]; then
ENV_VARS="$ENV_VARS STORAGE_DIR=$STORAGE_DIR"
fi
if [ -n "$ADMIN_PASSWORD" ]; then
ENV_VARS="$ENV_VARS ADMIN_PASSWORD=$ADMIN_PASSWORD"
fi
echo "启动服务..."
echo " 端口: $PORT"
if [ -n "$STORAGE_DIR" ]; then
echo " 存储: $STORAGE_DIR"
fi
echo " 命令: $ENV_VARS $CMD"
nohup sh -c "$ENV_VARS $CMD" > nohup.out 2>&1 &
echo "服务已启动 (PID: $!)"
echo "查看日志: tail -f nohup.out"

12
stop.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/bash
PID=$(pgrep -f "node src/index.js")
if [ -z "$PID" ]; then
echo "服务未运行"
exit 1
fi
echo "停止服务 (PID: $PID)..."
kill $PID
echo "服务已停止"