Compare commits
7 Commits
bd0c22cd73
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd5d2fcd70 | ||
|
|
384e437440 | ||
|
|
f8fbd290c7 | ||
|
|
e53a674bff | ||
|
|
b2ed9002dd | ||
|
|
b885dbac0f | ||
|
|
fdae636637 |
13
DESIGN.md
13
DESIGN.md
@@ -12,15 +12,16 @@ npm install
|
||||
npm start # 默认 3000 端口, ./storage 目录
|
||||
PORT=8080 npm start # 指定端口
|
||||
STORAGE_DIR=/data myoss # 指定存储目录
|
||||
PORT=8080 STORAGE_DIR=/data myoss # 同时指定
|
||||
|
||||
# 首次启动需要设置管理员密码
|
||||
ADMIN_PASSWORD=yourpassword npm start
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 创建首个 API Key(bootstrap)
|
||||
### 初始化首个 API Key
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/keys/bootstrap -H "Content-Type: application/json" -d '{"name":"root"}'
|
||||
# 返回: {"key":"xxx","name":"root"}
|
||||
curl -X POST http://localhost:3000/api/keys/bootstrap \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"password":"yourpassword","name":"root"}'
|
||||
```
|
||||
|
||||
### 2. 上传文件
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
module.exports = {
|
||||
port: process.env.PORT || 3000,
|
||||
admin: {
|
||||
password: process.env.ADMIN_PASSWORD || '',
|
||||
},
|
||||
storage: {
|
||||
baseDir: process.env.STORAGE_DIR || './storage',
|
||||
get filesDir() { return this.baseDir + '/files'; },
|
||||
|
||||
33
package-lock.json
generated
33
package-lock.json
generated
@@ -13,6 +13,7 @@
|
||||
"express": "^4.18.2",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"sequelize": "^6.35.0",
|
||||
"sql.js": "^1.14.1",
|
||||
"sqlite3": "^6.0.1",
|
||||
"uuid": "^9.0.0"
|
||||
}
|
||||
@@ -278,10 +279,13 @@
|
||||
}
|
||||
},
|
||||
"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"
|
||||
"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/concat-stream": {
|
||||
"version": "1.6.2",
|
||||
@@ -1618,6 +1622,12 @@
|
||||
"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": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-6.0.1.tgz",
|
||||
@@ -1714,6 +1724,12 @@
|
||||
"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": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||
@@ -1744,15 +1760,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": {
|
||||
"version": "0.2.16",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"express": "^4.18.2",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"sequelize": "^6.35.0",
|
||||
"sql.js": "^1.14.1",
|
||||
"sqlite3": "^6.0.1",
|
||||
"uuid": "^9.0.0"
|
||||
}
|
||||
|
||||
248
public/index.html
Normal file
248
public/index.html
Normal file
@@ -0,0 +1,248 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>对象存储管理</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; padding: 20px; }
|
||||
.container { max-width: 800px; margin: 0 auto; }
|
||||
h1 { text-align: center; margin-bottom: 20px; color: #333; }
|
||||
.card { background: white; border-radius: 8px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||
.card h2 { margin-bottom: 15px; color: #444; font-size: 18px; }
|
||||
.form-group { margin-bottom: 15px; }
|
||||
label { display: block; margin-bottom: 5px; color: #666; }
|
||||
input, select { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; }
|
||||
button { background: #007aff; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-size: 14px; }
|
||||
button:hover { background: #005bb5; }
|
||||
button:disabled { background: #ccc; cursor: not-allowed; }
|
||||
.btn-secondary { background: #34c759; }
|
||||
.btn-secondary:hover { background: #28a745; }
|
||||
.btn-danger { background: #ff3b30; }
|
||||
.btn-danger:hover { background: #dc3545; }
|
||||
.row { display: flex; gap: 10px; }
|
||||
.row input { flex: 1; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { text-align: left; padding: 12px; border-bottom: 1px solid #eee; }
|
||||
th { color: #666; font-weight: 500; }
|
||||
.file-key { font-family: monospace; font-size: 12px; color: #666; }
|
||||
.actions { display: flex; gap: 8px; }
|
||||
.actions a, .actions button { padding: 6px 12px; font-size: 12px; text-decoration: none; border-radius: 4px; }
|
||||
.actions a { background: #007aff; color: white; }
|
||||
.actions button.danger { background: #ff3b30; color: white; border: none; }
|
||||
.api-key-box { background: #f0f0f0; padding: 15px; border-radius: 4px; word-break: break-all; font-family: monospace; }
|
||||
.empty { text-align: center; color: #999; padding: 20px; }
|
||||
.hidden { display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>对象存储管理</h1>
|
||||
|
||||
<div class="card" id="setup-card">
|
||||
<h2>初始化</h2>
|
||||
<div class="form-group">
|
||||
<label>名称</label>
|
||||
<input type="text" id="setup-name" placeholder="输入名称" value="root">
|
||||
</div>
|
||||
<button onclick="setup()">创建首个 API Key</button>
|
||||
</div>
|
||||
|
||||
<div class="card hidden" id="main-card">
|
||||
<h2>当前 API Key</h2>
|
||||
<div class="api-key-box" id="current-key"></div>
|
||||
<div style="margin-top: 10px;">
|
||||
<button class="btn-secondary" onclick="createKey()">创建新 Key</button>
|
||||
<button class="btn-danger" onclick="switchKey()">切换 Key</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card hidden" id="upload-card">
|
||||
<h2>上传文件</h2>
|
||||
<div class="form-group">
|
||||
<input type="file" id="file-input" multiple>
|
||||
</div>
|
||||
<button onclick="uploadFile()">上传</button>
|
||||
</div>
|
||||
|
||||
<div class="card hidden" id="files-card">
|
||||
<h2>文件列表</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>文件名</th>
|
||||
<th>大小</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="files-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let apiKey = localStorage.getItem('myoss_api_key');
|
||||
const API_BASE = '';
|
||||
|
||||
async function request(path, options = {}) {
|
||||
const headers = { ...options.headers };
|
||||
if (apiKey) headers['X-API-Key'] = apiKey;
|
||||
const res = await fetch(API_BASE + path, { ...options, headers });
|
||||
if (res.status === 401) {
|
||||
alert('API Key 无效');
|
||||
localStorage.removeItem('myoss_api_key');
|
||||
apiKey = null;
|
||||
location.reload();
|
||||
return null;
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function setup() {
|
||||
const savedKey = localStorage.getItem('myoss_api_key');
|
||||
if (savedKey) {
|
||||
apiKey = savedKey;
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否已有 key
|
||||
const password = prompt('请输入管理员密码:');
|
||||
if (!password) return;
|
||||
|
||||
const name = document.getElementById('setup-name').value || 'root';
|
||||
const res = await fetch('/api/keys/bootstrap', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password, name })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
return;
|
||||
}
|
||||
apiKey = data.key;
|
||||
localStorage.setItem('myoss_api_key', apiKey);
|
||||
location.reload();
|
||||
}
|
||||
|
||||
async function createKey() {
|
||||
const name = prompt('输入名称', 'new-key');
|
||||
if (!name) return;
|
||||
const data = await request('/api/keys', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name })
|
||||
});
|
||||
alert('新 Key: ' + data.key);
|
||||
apiKey = data.key;
|
||||
localStorage.setItem('myoss_api_key', apiKey);
|
||||
location.reload();
|
||||
}
|
||||
|
||||
function switchKey() {
|
||||
const key = prompt('输入 API Key', apiKey);
|
||||
if (key) {
|
||||
apiKey = key;
|
||||
localStorage.setItem('myoss_api_key', apiKey);
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadFile() {
|
||||
const input = document.getElementById('file-input');
|
||||
const files = input.files;
|
||||
if (!files.length) return alert('请选择文件');
|
||||
|
||||
for (const file of files) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const data = await request('/api/files', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
alert('上传成功');
|
||||
loadFiles();
|
||||
}
|
||||
|
||||
async function loadFiles() {
|
||||
const files = await request('/api/files');
|
||||
renderFiles(files);
|
||||
}
|
||||
|
||||
function renderFiles(files) {
|
||||
const tbody = document.getElementById('files-tbody');
|
||||
if (!files || !files.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="3" class="empty">暂无文件</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = files.map(f => `
|
||||
<tr>
|
||||
<td>
|
||||
<div>${f.filename}</div>
|
||||
<div class="file-key">${f.fileKey}</div>
|
||||
</td>
|
||||
<td>${formatSize(f.size)}</td>
|
||||
<td class="actions">
|
||||
<a href="/api/files/${f.fileKey}/download?key=${apiKey}" target="_blank">下载</a>
|
||||
<button class="danger" onclick="deleteFile('${f.fileKey}')">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function deleteFile(fileKey) {
|
||||
if (!confirm('确定删除?')) return;
|
||||
await request(`/api/files/${fileKey}`, { method: 'DELETE' });
|
||||
loadFiles();
|
||||
}
|
||||
|
||||
function formatSize(bytes) {
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
if (bytes < 1024 * 1024 * 1024) return (bytes / 1024 / 1024).toFixed(1) + ' MB';
|
||||
return (bytes / 1024 / 1024 / 1024).toFixed(1) + ' GB';
|
||||
}
|
||||
|
||||
async function init() {
|
||||
if (!apiKey) {
|
||||
// 先检查是否已有 bootstrap key
|
||||
try {
|
||||
const res = await fetch('/api/files', {
|
||||
headers: { 'X-API-Key': 'check' }
|
||||
});
|
||||
if (res.status === 401 || res.status === 404) {
|
||||
document.getElementById('setup-card').classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
} catch (e) {}
|
||||
document.getElementById('setup-card').classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const files = await request('/api/files');
|
||||
if (!files) {
|
||||
document.getElementById('setup-card').classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
document.getElementById('main-card').classList.remove('hidden');
|
||||
document.getElementById('upload-card').classList.remove('hidden');
|
||||
document.getElementById('files-card').classList.remove('hidden');
|
||||
document.getElementById('current-key').textContent = apiKey;
|
||||
renderFiles(files);
|
||||
} catch (e) {
|
||||
document.getElementById('setup-card').classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
34
run.js
Normal file
34
run.js
Normal 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);
|
||||
});
|
||||
42
src/index.js
42
src/index.js
@@ -1,21 +1,9 @@
|
||||
const express = require('express');
|
||||
const { init } = require('./models');
|
||||
const config = require('../config');
|
||||
const { sequelize } = require('./models');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
const routes = require('./routes');
|
||||
|
||||
app.use('/api', routes);
|
||||
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok' });
|
||||
});
|
||||
|
||||
async function start() {
|
||||
(async () => {
|
||||
const dirs = [config.storage.filesDir, config.storage.uploadsDir];
|
||||
for (const dir of dirs) {
|
||||
if (!fs.existsSync(dir)) {
|
||||
@@ -23,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, () => {
|
||||
console.log(`Server running on port ${config.port}`);
|
||||
});
|
||||
}
|
||||
|
||||
start().catch(console.error);
|
||||
})().catch(e => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
36
src/index.js.bak
Normal file
36
src/index.js.bak
Normal 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);
|
||||
@@ -1,12 +1,20 @@
|
||||
const { APIKey } = require('../models');
|
||||
let models;
|
||||
|
||||
const setModels = (m) => { models = m; };
|
||||
const getModels = () => models;
|
||||
|
||||
const authMiddleware = async (req, res, next) => {
|
||||
const apiKey = req.headers['x-api-key'];
|
||||
const apiKey = req.headers['x-api-key'] || req.query.key;
|
||||
if (!apiKey) {
|
||||
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) {
|
||||
return res.status(401).json({ error: 'Invalid API Key' });
|
||||
}
|
||||
@@ -15,4 +23,4 @@ const authMiddleware = async (req, res, next) => {
|
||||
next();
|
||||
};
|
||||
|
||||
module.exports = authMiddleware;
|
||||
module.exports = { authMiddleware, setModels, getModels };
|
||||
@@ -1,49 +1,64 @@
|
||||
const { Sequelize, DataTypes } = require('sequelize');
|
||||
const config = require('../../config');
|
||||
const initSqlJs = require('sql.js');
|
||||
|
||||
const sequelize = new Sequelize({
|
||||
dialect: 'sqlite',
|
||||
storage: config.storage.databasePath,
|
||||
logging: false,
|
||||
});
|
||||
let sequelize;
|
||||
let models;
|
||||
|
||||
const APIKey = sequelize.define('APIKey', {
|
||||
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
|
||||
key: { type: DataTypes.STRING(64), unique: true, allowNull: false },
|
||||
name: { type: DataTypes.STRING, allowNull: false },
|
||||
ownerId: { type: DataTypes.INTEGER, defaultValue: 0 },
|
||||
});
|
||||
const init = async () => {
|
||||
if (models) return models;
|
||||
|
||||
const SQL = await initSqlJs();
|
||||
sequelize = new Sequelize({
|
||||
dialect: 'sqlite',
|
||||
storage: config.storage.databasePath,
|
||||
logging: false,
|
||||
dialectModule: SQL,
|
||||
});
|
||||
|
||||
const File = sequelize.define('File', {
|
||||
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
|
||||
fileKey: { type: DataTypes.STRING(64), unique: true, allowNull: false },
|
||||
filename: { type: DataTypes.STRING, allowNull: false },
|
||||
size: { type: DataTypes.INTEGER, allowNull: false },
|
||||
mimeType: { type: DataTypes.STRING },
|
||||
storagePath: { type: DataTypes.STRING, allowNull: false },
|
||||
}, { tableName: 'files' });
|
||||
const APIKey = sequelize.define('APIKey', {
|
||||
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
|
||||
key: { type: DataTypes.STRING(64), unique: true, allowNull: false },
|
||||
name: { type: DataTypes.STRING, allowNull: false },
|
||||
ownerId: { type: DataTypes.INTEGER, defaultValue: 0 },
|
||||
});
|
||||
|
||||
const Chunk = sequelize.define('Chunk', {
|
||||
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' });
|
||||
const File = sequelize.define('File', {
|
||||
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
|
||||
fileKey: { type: DataTypes.STRING(64), unique: true, allowNull: false },
|
||||
filename: { type: DataTypes.STRING, allowNull: false },
|
||||
size: { type: DataTypes.INTEGER, allowNull: false },
|
||||
mimeType: { type: DataTypes.STRING },
|
||||
storagePath: { type: DataTypes.STRING, allowNull: false },
|
||||
}, { tableName: 'files' });
|
||||
|
||||
const UploadSession = sequelize.define('UploadSession', {
|
||||
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' });
|
||||
const Chunk = sequelize.define('Chunk', {
|
||||
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(File, { foreignKey: 'ownerId' });
|
||||
File.belongsTo(APIKey, { foreignKey: 'ownerId' });
|
||||
const UploadSession = sequelize.define('UploadSession', {
|
||||
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' });
|
||||
|
||||
APIKey.hasMany(UploadSession, { foreignKey: 'ownerId' });
|
||||
UploadSession.belongsTo(APIKey, { foreignKey: 'ownerId' });
|
||||
APIKey.hasMany(File, { foreignKey: 'ownerId' });
|
||||
File.belongsTo(APIKey, { foreignKey: 'ownerId' });
|
||||
|
||||
module.exports = { sequelize, APIKey, File, Chunk, UploadSession };
|
||||
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
134
src/models/sqlite.js
Normal 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 };
|
||||
@@ -1,8 +1,7 @@
|
||||
const express = require('express');
|
||||
const { v4: uuidv4 } = require('crypto-js');
|
||||
const CryptoJS = require('crypto-js');
|
||||
const { APIKey, File, Chunk, UploadSession } = require('../models');
|
||||
const authMiddleware = require('../middleware/auth');
|
||||
const { init } = require('../models/sqlite');
|
||||
const { setModels, authMiddleware } = require('../middleware/auth');
|
||||
const config = require('../../config');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
@@ -11,15 +10,32 @@ const router = express.Router();
|
||||
|
||||
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) => {
|
||||
const count = await APIKey.count();
|
||||
const { APIKey } = getModels();
|
||||
const count = APIKey.count();
|
||||
if (count > 0) {
|
||||
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 name = req.body.name || 'Root';
|
||||
const apiKey = await APIKey.create({ key, name, ownerId: 0 });
|
||||
const keyName = name || 'Root';
|
||||
const apiKey = APIKey.create({ key, name: keyName, ownerId: 0 });
|
||||
|
||||
const dir = path.join(config.storage.filesDir, 'root');
|
||||
if (!fs.existsSync(dir)) {
|
||||
@@ -32,16 +48,18 @@ router.post('/keys/bootstrap', async (req, res) => {
|
||||
router.use(authMiddleware);
|
||||
|
||||
router.get('/keys', async (req, res) => {
|
||||
const { APIKey } = getModels();
|
||||
const ownerId = req.apiKey.ownerId || req.apiKey.id;
|
||||
const keys = await APIKey.findAll({ where: { ownerId } });
|
||||
const keys = APIKey.findAll({ where: { ownerId } });
|
||||
res.json(keys);
|
||||
});
|
||||
|
||||
router.post('/keys', async (req, res) => {
|
||||
const { APIKey } = getModels();
|
||||
const key = CryptoJS.lib.WordArray.random(16).toString();
|
||||
const name = req.body.name || 'Unnamed';
|
||||
const ownerId = getOwnerId(req.apiKey);
|
||||
const apiKey = await APIKey.create({ key, name, ownerId });
|
||||
const apiKey = APIKey.create({ key, name, ownerId });
|
||||
|
||||
const dir = ownerId === 0
|
||||
? path.join(config.storage.filesDir, 'root')
|
||||
@@ -54,24 +72,25 @@ router.post('/keys', async (req, res) => {
|
||||
});
|
||||
|
||||
router.delete('/keys/:keyId', async (req, res) => {
|
||||
const { APIKey } = getModels();
|
||||
const ownerId = getOwnerId(req.apiKey);
|
||||
const apiKey = await APIKey.findOne({
|
||||
where: { id: req.params.keyId, ownerId }
|
||||
});
|
||||
const apiKey = APIKey.findOne({ where: { id: parseInt(req.params.keyId), ownerId } });
|
||||
if (!apiKey) {
|
||||
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();
|
||||
});
|
||||
|
||||
router.get('/files', async (req, res) => {
|
||||
const { File } = getModels();
|
||||
const ownerId = req.apiKey.ownerId || req.apiKey.id;
|
||||
const files = await File.findAll({ where: { ownerId } });
|
||||
const files = File.findAll({ where: { ownerId } });
|
||||
res.json(files);
|
||||
});
|
||||
|
||||
router.post('/files', async (req, res) => {
|
||||
const { File } = getModels();
|
||||
const multer = require('multer');
|
||||
const getDir = (key) => {
|
||||
const oid = getOwnerId(key);
|
||||
@@ -101,9 +120,9 @@ router.post('/files', async (req, res) => {
|
||||
const { filename, size, mimeType, path: storagePath } = req.file;
|
||||
const fileKey = path.basename(storagePath);
|
||||
|
||||
const file = await File.create({
|
||||
const file = File.create({
|
||||
fileKey,
|
||||
filename: req.file.originalname,
|
||||
filename: req.file.originalname,
|
||||
size,
|
||||
mimeType: mimeType || 'application/octet-stream',
|
||||
storagePath,
|
||||
@@ -115,10 +134,9 @@ filename: req.file.originalname,
|
||||
});
|
||||
|
||||
router.get('/files/:fileKey', async (req, res) => {
|
||||
const { File } = getModels();
|
||||
const ownerId = getOwnerId(req.apiKey);
|
||||
const file = await File.findOne({
|
||||
where: { fileKey: req.params.fileKey, ownerId }
|
||||
});
|
||||
const file = File.findOne({ where: { fileKey: req.params.fileKey, ownerId } });
|
||||
if (!file) {
|
||||
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) => {
|
||||
const { File } = getModels();
|
||||
const ownerId = getOwnerId(req.apiKey);
|
||||
const file = await File.findOne({
|
||||
where: { fileKey: req.params.fileKey, ownerId }
|
||||
});
|
||||
const file = File.findOne({ where: { fileKey: req.params.fileKey, ownerId } });
|
||||
if (!file) {
|
||||
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) => {
|
||||
const { File } = getModels();
|
||||
const ownerId = getOwnerId(req.apiKey);
|
||||
const file = await File.findOne({
|
||||
where: { fileKey: req.params.fileKey, ownerId }
|
||||
});
|
||||
const file = File.findOne({ where: { fileKey: req.params.fileKey, ownerId } });
|
||||
if (!file) {
|
||||
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) => {
|
||||
const { File } = getModels();
|
||||
const ownerId = getOwnerId(req.apiKey);
|
||||
const file = await File.findOne({
|
||||
where: { fileKey: req.params.fileKey, ownerId }
|
||||
});
|
||||
const file = File.findOne({ where: { fileKey: req.params.fileKey, ownerId } });
|
||||
if (!file) {
|
||||
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)) {
|
||||
fs.unlinkSync(file.storagePath);
|
||||
}
|
||||
await file.destroy();
|
||||
File.destroy({ where: { fileKey: req.params.fileKey, ownerId } });
|
||||
res.status(204).send();
|
||||
});
|
||||
|
||||
router.post('/uploads/init', async (req, res) => {
|
||||
const { UploadSession } = getModels();
|
||||
const { filename, totalSize, chunkSize, totalChunks, mimeType } = req.body;
|
||||
|
||||
if (!filename || !totalSize || !totalChunks) {
|
||||
return res.status(400).json({ error: 'Missing required fields' });
|
||||
}
|
||||
|
||||
const ownerId = getOwnerId(req.apiKey);
|
||||
const uploadId = CryptoJS.lib.WordArray.random(16).toString();
|
||||
|
||||
const session = await UploadSession.create({
|
||||
const session = UploadSession.create({
|
||||
id: uploadId,
|
||||
filename,
|
||||
totalSize,
|
||||
chunkSize: chunkSize || config.upload.maxChunkSize,
|
||||
totalChunks,
|
||||
mimeType,
|
||||
ownerId,
|
||||
ownerId: getOwnerId(req.apiKey),
|
||||
});
|
||||
|
||||
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) => {
|
||||
const { UploadSession, Chunk } = getModels();
|
||||
const { uploadId } = req.params;
|
||||
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' });
|
||||
}
|
||||
|
||||
const session = await UploadSession.findOne({
|
||||
where: { id: uploadId, ownerId: getOwnerId(req.apiKey) }
|
||||
});
|
||||
const session = UploadSession.findOne({ where: { id: uploadId, ownerId: getOwnerId(req.apiKey) } });
|
||||
if (!session) {
|
||||
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;
|
||||
|
||||
await Chunk.create({
|
||||
Chunk.create({
|
||||
uploadId,
|
||||
chunkIndex,
|
||||
size,
|
||||
@@ -242,19 +256,15 @@ router.post('/uploads/:uploadId/chunk', async (req, res) => {
|
||||
});
|
||||
|
||||
router.post('/uploads/:uploadId/complete', async (req, res) => {
|
||||
const { UploadSession, Chunk, File } = getModels();
|
||||
const { uploadId } = req.params;
|
||||
|
||||
const session = await UploadSession.findOne({
|
||||
where: { id: uploadId, ownerId: getOwnerId(req.apiKey) }
|
||||
});
|
||||
const session = UploadSession.findOne({ where: { id: uploadId, ownerId: getOwnerId(req.apiKey) } });
|
||||
if (!session) {
|
||||
return res.status(404).json({ error: 'Upload session not found' });
|
||||
}
|
||||
|
||||
const chunks = await Chunk.findAll({
|
||||
where: { uploadId },
|
||||
order: [['chunkIndex', 'ASC']],
|
||||
});
|
||||
const chunks = Chunk.findAll({ where: { uploadId }, order: [['chunkIndex', 'ASC']] });
|
||||
|
||||
if (chunks.length !== session.totalChunks) {
|
||||
return res.status(400).json({
|
||||
@@ -290,17 +300,17 @@ const session = await UploadSession.findOne({
|
||||
}
|
||||
fs.rmdirSync(path.join(config.storage.uploadsDir, uploadId));
|
||||
|
||||
const file = await File.create({
|
||||
const file = File.create({
|
||||
fileKey,
|
||||
filename: session.filename,
|
||||
size: session.totalSize,
|
||||
mimeType: session.mimeType || 'application/octet-stream',
|
||||
storagePath: finalPath,
|
||||
ownerId: ownerId,
|
||||
ownerId,
|
||||
});
|
||||
|
||||
await session.destroy();
|
||||
await Chunk.destroy({ where: { uploadId } });
|
||||
UploadSession.destroy({ where: { id: uploadId } });
|
||||
Chunk.destroy({ where: { uploadId } });
|
||||
|
||||
res.status(201).json(file);
|
||||
});
|
||||
|
||||
65
start.sh
Executable file
65
start.sh
Executable 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"
|
||||
Reference in New Issue
Block a user