nas-samba v2.0.0: per-user home directories, Samba accounts, remove quotas
- Each user gets a Linux + Samba account and home dir at /nas/home/<username>/ - Web UI file browsing scoped to per-user directories - Creating/deleting users from web UI also manages system + Samba accounts - Replaced hardcoded [data] SMB share with [homes] (per-user automatic shares) - Removed hardcoded alexz user from entrypoint — server.js handles all user management - Removed storage quotas from backend and frontend - Existing users get home directories auto-created on startup (migration)
This commit is contained in:
@@ -8,7 +8,7 @@
|
|||||||
"dynamic_config": true,
|
"dynamic_config": true,
|
||||||
"no_gui": false,
|
"no_gui": false,
|
||||||
"tipi_version": 2,
|
"tipi_version": 2,
|
||||||
"version": "1.0.2",
|
"version": "2.0.0",
|
||||||
"categories": ["network", "utilities"],
|
"categories": ["network", "utilities"],
|
||||||
"description": "All-in-one NAS container with Samba SMB file sharing and CloudNAS web file manager. Browse files via web UI or connect via SMB at \\\\192.168.0.15\\data with username alexz. No database required — uses filesystem + SQLite.",
|
"description": "All-in-one NAS container with Samba SMB file sharing and CloudNAS web file manager. Browse files via web UI or connect via SMB at \\\\192.168.0.15\\data with username alexz. No database required — uses filesystem + SQLite.",
|
||||||
"short_desc": "NAS with SMB + web file manager",
|
"short_desc": "NAS with SMB + web file manager",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"services": [
|
"services": [
|
||||||
{
|
{
|
||||||
"name": "nas-samba",
|
"name": "nas-samba",
|
||||||
"image": "git.alexzaw.dev/alexz/nas-samba:1.0.2",
|
"image": "git.alexzaw.dev/alexz/nas-samba:2.0.0",
|
||||||
"isMain": true,
|
"isMain": true,
|
||||||
"internalPort": 8080,
|
"internalPort": 8080,
|
||||||
"hostname": "nas.alexzaw.dev",
|
"hostname": "nas.alexzaw.dev",
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"files": {
|
"files": {
|
||||||
"main.css": "/static/css/main.f6816838.css",
|
"main.css": "/static/css/main.6f136b27.css",
|
||||||
"main.js": "/static/js/main.2e7681ef.js",
|
"main.js": "/static/js/main.f1998df4.js",
|
||||||
"index.html": "/index.html",
|
"index.html": "/index.html",
|
||||||
"main.f6816838.css.map": "/static/css/main.f6816838.css.map",
|
"main.6f136b27.css.map": "/static/css/main.6f136b27.css.map",
|
||||||
"main.2e7681ef.js.map": "/static/js/main.2e7681ef.js.map"
|
"main.f1998df4.js.map": "/static/js/main.f1998df4.js.map"
|
||||||
},
|
},
|
||||||
"entrypoints": [
|
"entrypoints": [
|
||||||
"static/css/main.f6816838.css",
|
"static/css/main.6f136b27.css",
|
||||||
"static/js/main.2e7681ef.js"
|
"static/js/main.f1998df4.js"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#0F172A"/><meta name="description" content="CloudSync - Enterprise File Manager"/><title>CloudSync - Enterprise File Manager</title><script defer="defer" src="/static/js/main.2e7681ef.js"></script><link href="/static/css/main.f6816838.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#0F172A"/><meta name="description" content="CloudSync - Enterprise File Manager"/><title>CloudSync - Enterprise File Manager</title><script defer="defer" src="/static/js/main.f1998df4.js"></script><link href="/static/css/main.6f136b27.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -8,7 +8,7 @@ import {
|
|||||||
MoreVertical, Download, Trash2, Edit3, Move, Copy,
|
MoreVertical, Download, Trash2, Edit3, Move, Copy,
|
||||||
Share2, ChevronRight, Home, Moon, Sun, LogOut, Users,
|
Share2, ChevronRight, Home, Moon, Sun, LogOut, Users,
|
||||||
Activity, X, Upload, Folder, File, Image,
|
Activity, X, Upload, Folder, File, Image,
|
||||||
Film, Music, Archive, Code, FileText, HardDrive,
|
Film, Music, Archive, Code, FileText,
|
||||||
Link, Shield
|
Link, Shield
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { formatBytes, formatDate, cn } from '../lib/utils';
|
import { formatBytes, formatDate, cn } from '../lib/utils';
|
||||||
@@ -159,42 +159,9 @@ function Modal({ open, onClose, title, children, size = 'md' }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Storage Usage Component
|
|
||||||
function StorageUsage({ used, quota }) {
|
|
||||||
const percentage = quota > 0 ? (used / quota) * 100 : 0;
|
|
||||||
const isWarning = percentage > 80;
|
|
||||||
const isCritical = percentage > 95;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-4 bg-card border border-border rounded-lg">
|
|
||||||
<div className="flex items-center justify-between mb-3">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<HardDrive className="w-4 h-4 text-muted-foreground" />
|
|
||||||
<span className="text-sm font-medium">Storage</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{formatBytes(used)} / {formatBytes(quota)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="h-2 bg-muted rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"h-full transition-all duration-300",
|
|
||||||
isCritical ? "bg-destructive" : isWarning ? "bg-amber-500" : "bg-primary"
|
|
||||||
)}
|
|
||||||
style={{ width: `${Math.min(percentage, 100)}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground mt-2">
|
|
||||||
{percentage.toFixed(1)}% used
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main Dashboard Component
|
// Main Dashboard Component
|
||||||
function Dashboard() {
|
function Dashboard() {
|
||||||
const { user, logout, updateUser } = useAuth();
|
const { user, logout } = useAuth();
|
||||||
const { theme, toggleTheme } = useTheme();
|
const { theme, toggleTheme } = useTheme();
|
||||||
|
|
||||||
// State
|
// State
|
||||||
@@ -232,7 +199,7 @@ function Dashboard() {
|
|||||||
const [loadingUsers, setLoadingUsers] = useState(false);
|
const [loadingUsers, setLoadingUsers] = useState(false);
|
||||||
const [showUserForm, setShowUserForm] = useState(false);
|
const [showUserForm, setShowUserForm] = useState(false);
|
||||||
const [editingUser, setEditingUser] = useState(null);
|
const [editingUser, setEditingUser] = useState(null);
|
||||||
const [userForm, setUserForm] = useState({ username: '', password: '', name: '', role: 'user', storage_quota: 10737418240 });
|
const [userForm, setUserForm] = useState({ username: '', password: '', name: '', role: 'user' });
|
||||||
|
|
||||||
// Activity state
|
// Activity state
|
||||||
const [activities, setActivities] = useState([]);
|
const [activities, setActivities] = useState([]);
|
||||||
@@ -274,11 +241,10 @@ function Dashboard() {
|
|||||||
try {
|
try {
|
||||||
const response = await api.getStats();
|
const response = await api.getStats();
|
||||||
setStats(response.data);
|
setStats(response.data);
|
||||||
updateUser({ storage_used: response.data.storage_used });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load stats');
|
console.error('Failed to load stats');
|
||||||
}
|
}
|
||||||
}, [updateUser]);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadFiles();
|
loadFiles();
|
||||||
@@ -467,7 +433,7 @@ function Dashboard() {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (editingUser) {
|
if (editingUser) {
|
||||||
const updateData = { name: userForm.name, role: userForm.role, storage_quota: userForm.storage_quota };
|
const updateData = { name: userForm.name, role: userForm.role };
|
||||||
if (userForm.password) updateData.password = userForm.password;
|
if (userForm.password) updateData.password = userForm.password;
|
||||||
await api.updateUser(editingUser.id, updateData);
|
await api.updateUser(editingUser.id, updateData);
|
||||||
toast.success('User updated');
|
toast.success('User updated');
|
||||||
@@ -477,7 +443,7 @@ function Dashboard() {
|
|||||||
}
|
}
|
||||||
setShowUserForm(false);
|
setShowUserForm(false);
|
||||||
setEditingUser(null);
|
setEditingUser(null);
|
||||||
setUserForm({ username: '', password: '', name: '', role: 'user', storage_quota: 10737418240 });
|
setUserForm({ username: '', password: '', name: '', role: 'user' });
|
||||||
loadUsers();
|
loadUsers();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(error.response?.data?.detail || 'Failed to save user');
|
toast.error(error.response?.data?.detail || 'Failed to save user');
|
||||||
@@ -661,12 +627,7 @@ function Dashboard() {
|
|||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 space-y-6">
|
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 space-y-6">
|
||||||
{/* Storage & Stats */}
|
{/* Storage & Stats */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<StorageUsage
|
|
||||||
used={stats?.storage_used || user?.storage_used || 0}
|
|
||||||
quota={stats?.storage_quota || user?.storage_quota || 10737418240}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="p-4 bg-card border border-border rounded-lg">
|
<div className="p-4 bg-card border border-border rounded-lg">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<File className="w-4 h-4 text-muted-foreground" />
|
<File className="w-4 h-4 text-muted-foreground" />
|
||||||
@@ -1212,24 +1173,9 @@ function Dashboard() {
|
|||||||
<option value="admin">Admin</option>
|
<option value="admin">Admin</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium mb-2">Storage Quota</label>
|
|
||||||
<select
|
|
||||||
value={userForm.storage_quota}
|
|
||||||
onChange={(e) => setUserForm(prev => ({ ...prev, storage_quota: Number(e.target.value) }))}
|
|
||||||
data-testid="user-quota-select"
|
|
||||||
className="w-full h-10 px-4 bg-background border border-input rounded-lg focus:outline-none focus:ring-2 focus:ring-ring"
|
|
||||||
>
|
|
||||||
<option value={1073741824}>1 GB</option>
|
|
||||||
<option value={5368709120}>5 GB</option>
|
|
||||||
<option value={10737418240}>10 GB</option>
|
|
||||||
<option value={53687091200}>50 GB</option>
|
|
||||||
<option value={107374182400}>100 GB</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-3 pt-2">
|
<div className="flex gap-3 pt-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => { setShowUserForm(false); setEditingUser(null); setUserForm({ username: '', password: '', name: '', role: 'user', storage_quota: 10737418240 }); }}
|
onClick={() => { setShowUserForm(false); setEditingUser(null); setUserForm({ username: '', password: '', name: '', role: 'user' }); }}
|
||||||
className="flex-1 h-10 bg-secondary text-secondary-foreground rounded-lg hover:opacity-80"
|
className="flex-1 h-10 bg-secondary text-secondary-foreground rounded-lg hover:opacity-80"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
@@ -1279,10 +1225,10 @@ function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{formatBytes(u.storage_used)} / {formatBytes(u.storage_quota)}
|
{u.role}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => { setEditingUser(u); setUserForm({ username: u.username, password: '', name: u.name, role: u.role, storage_quota: u.storage_quota }); setShowUserForm(true); }}
|
onClick={() => { setEditingUser(u); setUserForm({ username: u.username, password: '', name: u.name, role: u.role }); setShowUserForm(true); }}
|
||||||
className="p-2 hover:bg-accent rounded-lg"
|
className="p-2 hover:bg-accent rounded-lg"
|
||||||
data-testid={`edit-user-${u.id}`}
|
data-testid={`edit-user-${u.id}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -17,12 +17,13 @@ const path = require('path');
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const archiver = require('archiver');
|
const archiver = require('archiver');
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Configuration
|
// Configuration
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
const PORT = process.env.PORT || 8080;
|
const PORT = process.env.PORT || 8080;
|
||||||
const UPLOAD_DIR = path.resolve(process.env.UPLOAD_DIR || '/nas');
|
const HOME_BASE = '/nas/home';
|
||||||
const DB_PATH = process.env.DB_PATH || '/config/cloudsync.db';
|
const DB_PATH = process.env.DB_PATH || '/config/cloudsync.db';
|
||||||
const JWT_SECRET = process.env.JWT_SECRET || 'cloudsync_secret_key';
|
const JWT_SECRET = process.env.JWT_SECRET || 'cloudsync_secret_key';
|
||||||
const JWT_ALGORITHM = 'HS256';
|
const JWT_ALGORITHM = 'HS256';
|
||||||
@@ -90,7 +91,7 @@ function initDatabase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Multer setup (disk storage into UPLOAD_DIR)
|
// Multer setup (disk storage into per-user dir)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
const upload = multer({ storage: multer.memoryStorage() });
|
const upload = multer({ storage: multer.memoryStorage() });
|
||||||
|
|
||||||
@@ -128,22 +129,22 @@ function decodeFileId(fileId) {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Helper: Path security - prevent directory traversal
|
// Helper: Path security - prevent directory traversal
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
function safePath(relativePath) {
|
function safePath(baseDir, relativePath) {
|
||||||
const resolved = path.resolve(UPLOAD_DIR, relativePath);
|
const resolved = path.resolve(baseDir, relativePath);
|
||||||
if (!resolved.startsWith(UPLOAD_DIR)) {
|
if (!resolved.startsWith(baseDir)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
function safeRelativePath(relativePath) {
|
function safeRelativePath(baseDir, relativePath) {
|
||||||
if (!relativePath) return '';
|
if (!relativePath) return '';
|
||||||
const normalized = path.normalize(relativePath);
|
const normalized = path.normalize(relativePath);
|
||||||
if (normalized.startsWith('..') || path.isAbsolute(normalized)) {
|
if (normalized.startsWith('..') || path.isAbsolute(normalized)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const full = path.resolve(UPLOAD_DIR, normalized);
|
const full = path.resolve(baseDir, normalized);
|
||||||
if (!full.startsWith(UPLOAD_DIR)) {
|
if (!full.startsWith(baseDir)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return normalized;
|
return normalized;
|
||||||
@@ -160,6 +161,42 @@ function verifyPassword(plainPassword, hashedPassword) {
|
|||||||
return bcrypt.compareSync(plainPassword, hashedPassword);
|
return bcrypt.compareSync(plainPassword, hashedPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helper: Username validation
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
function isValidUsername(username) {
|
||||||
|
return /^[a-z_][a-z0-9_-]{0,31}$/.test(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helper: System user management (Linux + Samba)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
function createSystemUser(username, password) {
|
||||||
|
execSync(`id -u "${username}" >/dev/null 2>&1 || useradd -M -s /usr/sbin/nologin "${username}"`);
|
||||||
|
const homeDir = path.join(HOME_BASE, username);
|
||||||
|
fs.mkdirSync(homeDir, { recursive: true });
|
||||||
|
execSync(`chown "${username}":nogroup "${homeDir}"`);
|
||||||
|
const escaped = password.replace(/'/g, "'\\''");
|
||||||
|
execSync(`printf '%s\\n%s\\n' '${escaped}' '${escaped}' | smbpasswd -a -s "${username}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteSystemUser(username) {
|
||||||
|
execSync(`smbpasswd -x "${username}" 2>/dev/null || true`);
|
||||||
|
execSync(`userdel "${username}" 2>/dev/null || true`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSambaPassword(username, password) {
|
||||||
|
const escaped = password.replace(/'/g, "'\\''");
|
||||||
|
execSync(`printf '%s\\n%s\\n' '${escaped}' '${escaped}' | smbpasswd -s "${username}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helper: Per-user directory
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
function getUserDir(username) {
|
||||||
|
return path.join(HOME_BASE, username);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Helper: JWT
|
// Helper: JWT
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -264,9 +301,8 @@ function countFilesAndFolders(dirPath) {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Helper: Get user storage used
|
// Helper: Get user storage used
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
function getUserStorageUsed(_userId) {
|
function getUserStorageUsed(username) {
|
||||||
// In single-directory mode, walk UPLOAD_DIR
|
return getDirSize(getUserDir(username));
|
||||||
return getDirSize(UPLOAD_DIR);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -293,7 +329,7 @@ function getFileIcon(filename, isFolder) {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Helper: Build file response from fs.stat result
|
// Helper: Build file response from fs.stat result
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
function formatFileResponse(relativePath, stat, parentRelPath) {
|
function formatFileResponse(userDir, relativePath, stat, parentRelPath) {
|
||||||
const name = path.basename(relativePath);
|
const name = path.basename(relativePath);
|
||||||
const isFolder = stat.isDirectory();
|
const isFolder = stat.isDirectory();
|
||||||
const mimeType = isFolder ? '' : (mime.lookup(name) || 'application/octet-stream');
|
const mimeType = isFolder ? '' : (mime.lookup(name) || 'application/octet-stream');
|
||||||
@@ -307,7 +343,7 @@ function formatFileResponse(relativePath, stat, parentRelPath) {
|
|||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
is_folder: isFolder,
|
is_folder: isFolder,
|
||||||
size: isFolder ? getDirSize(path.join(UPLOAD_DIR, relativePath)) : stat.size,
|
size: isFolder ? getDirSize(path.join(userDir, relativePath)) : stat.size,
|
||||||
mime_type: mimeType,
|
mime_type: mimeType,
|
||||||
icon: getFileIcon(name, isFolder),
|
icon: getFileIcon(name, isFolder),
|
||||||
parent_id: parentId,
|
parent_id: parentId,
|
||||||
@@ -321,7 +357,7 @@ function formatFileResponse(relativePath, stat, parentRelPath) {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Helper: Search files recursively
|
// Helper: Search files recursively
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
function searchFiles(dirPath, query, basePath) {
|
function searchFiles(dirPath, query, basePath, userDir) {
|
||||||
const results = [];
|
const results = [];
|
||||||
if (!fs.existsSync(dirPath)) return results;
|
if (!fs.existsSync(dirPath)) return results;
|
||||||
const lowerQuery = query.toLowerCase();
|
const lowerQuery = query.toLowerCase();
|
||||||
@@ -335,14 +371,14 @@ function searchFiles(dirPath, query, basePath) {
|
|||||||
try {
|
try {
|
||||||
const stat = fs.statSync(fullPath);
|
const stat = fs.statSync(fullPath);
|
||||||
const parentRel = basePath || '';
|
const parentRel = basePath || '';
|
||||||
results.push(formatFileResponse(relativePath, stat, parentRel || null));
|
results.push(formatFileResponse(userDir, relativePath, stat, parentRel || null));
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
// skip
|
// skip
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
results.push(...searchFiles(fullPath, query, relativePath));
|
results.push(...searchFiles(fullPath, query, relativePath, userDir));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
@@ -460,8 +496,6 @@ app.post('/api/auth/login', (req, res) => {
|
|||||||
|
|
||||||
logActivity(user.id, 'login', 'auth', user.username);
|
logActivity(user.id, 'login', 'auth', user.username);
|
||||||
|
|
||||||
const storageUsed = getUserStorageUsed(user.id);
|
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
token,
|
token,
|
||||||
user: {
|
user: {
|
||||||
@@ -469,8 +503,6 @@ app.post('/api/auth/login', (req, res) => {
|
|||||||
username: user.username,
|
username: user.username,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
role: user.role,
|
role: user.role,
|
||||||
storage_quota: user.storage_quota || 10737418240,
|
|
||||||
storage_used: storageUsed,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -481,14 +513,11 @@ app.post('/api/auth/login', (req, res) => {
|
|||||||
|
|
||||||
app.get('/api/auth/me', requireAuth, (req, res) => {
|
app.get('/api/auth/me', requireAuth, (req, res) => {
|
||||||
try {
|
try {
|
||||||
const storageUsed = getUserStorageUsed(req.user.id);
|
|
||||||
return res.json({
|
return res.json({
|
||||||
id: req.user.id,
|
id: req.user.id,
|
||||||
username: req.user.username,
|
username: req.user.username,
|
||||||
name: req.user.name,
|
name: req.user.name,
|
||||||
role: req.user.role,
|
role: req.user.role,
|
||||||
storage_quota: req.user.storage_quota || 10737418240,
|
|
||||||
storage_used: storageUsed,
|
|
||||||
created_at: req.user.created_at || '',
|
created_at: req.user.created_at || '',
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -502,14 +531,12 @@ app.get('/api/auth/me', requireAuth, (req, res) => {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
app.get('/api/users', requireAuth, requireAdmin, (_req, res) => {
|
app.get('/api/users', requireAuth, requireAdmin, (_req, res) => {
|
||||||
try {
|
try {
|
||||||
const users = sqlDb.prepare('SELECT id, username, name, role, storage_quota, created_at FROM users').all();
|
const users = sqlDb.prepare('SELECT id, username, name, role, created_at FROM users').all();
|
||||||
const result = users.map((u) => ({
|
const result = users.map((u) => ({
|
||||||
id: u.id,
|
id: u.id,
|
||||||
username: u.username,
|
username: u.username,
|
||||||
name: u.name,
|
name: u.name,
|
||||||
role: u.role,
|
role: u.role,
|
||||||
storage_quota: u.storage_quota || 10737418240,
|
|
||||||
storage_used: getUserStorageUsed(u.id),
|
|
||||||
created_at: u.created_at || '',
|
created_at: u.created_at || '',
|
||||||
}));
|
}));
|
||||||
return res.json(result);
|
return res.json(result);
|
||||||
@@ -521,11 +548,15 @@ app.get('/api/users', requireAuth, requireAdmin, (_req, res) => {
|
|||||||
|
|
||||||
app.post('/api/users', requireAuth, requireAdmin, (req, res) => {
|
app.post('/api/users', requireAuth, requireAdmin, (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { username, password, name, role, storage_quota } = req.body;
|
const { username, password, name, role } = req.body;
|
||||||
if (!username || !password || !name) {
|
if (!username || !password || !name) {
|
||||||
return res.status(400).json({ detail: 'Username, password, and name are required' });
|
return res.status(400).json({ detail: 'Username, password, and name are required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isValidUsername(username)) {
|
||||||
|
return res.status(400).json({ detail: 'Username must be lowercase alphanumeric, start with letter or underscore, max 32 chars' });
|
||||||
|
}
|
||||||
|
|
||||||
const existing = sqlDb.prepare('SELECT id FROM users WHERE username = ?').get(username);
|
const existing = sqlDb.prepare('SELECT id FROM users WHERE username = ?').get(username);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
return res.status(400).json({ detail: 'Username already exists' });
|
return res.status(400).json({ detail: 'Username already exists' });
|
||||||
@@ -534,7 +565,15 @@ app.post('/api/users', requireAuth, requireAdmin, (req, res) => {
|
|||||||
const userId = crypto.randomUUID();
|
const userId = crypto.randomUUID();
|
||||||
sqlDb.prepare(
|
sqlDb.prepare(
|
||||||
'INSERT INTO users (id, username, password_hash, name, role, storage_quota, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
'INSERT INTO users (id, username, password_hash, name, role, storage_quota, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
||||||
).run(userId, username, hashPassword(password), name, role || 'user', storage_quota || 10737418240, new Date().toISOString());
|
).run(userId, username, hashPassword(password), name, role || 'user', 10737418240, new Date().toISOString());
|
||||||
|
|
||||||
|
try {
|
||||||
|
createSystemUser(username, password);
|
||||||
|
} catch (sysErr) {
|
||||||
|
console.error('Failed to create system user:', sysErr.message);
|
||||||
|
sqlDb.prepare('DELETE FROM users WHERE id = ?').run(userId);
|
||||||
|
return res.status(500).json({ detail: 'Failed to create system user' });
|
||||||
|
}
|
||||||
|
|
||||||
logActivity(req.user.id, 'create', 'user', username);
|
logActivity(req.user.id, 'create', 'user', username);
|
||||||
|
|
||||||
@@ -548,7 +587,7 @@ app.post('/api/users', requireAuth, requireAdmin, (req, res) => {
|
|||||||
app.put('/api/users/:userId', requireAuth, requireAdmin, (req, res) => {
|
app.put('/api/users/:userId', requireAuth, requireAdmin, (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { userId } = req.params;
|
const { userId } = req.params;
|
||||||
const { name, role, password, storage_quota } = req.body;
|
const { name, role, password } = req.body;
|
||||||
|
|
||||||
const user = sqlDb.prepare('SELECT * FROM users WHERE id = ?').get(userId);
|
const user = sqlDb.prepare('SELECT * FROM users WHERE id = ?').get(userId);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@@ -560,8 +599,15 @@ app.put('/api/users/:userId', requireAuth, requireAdmin, (req, res) => {
|
|||||||
|
|
||||||
if (name !== undefined && name !== null) { sets.push('name = ?'); params.push(name); }
|
if (name !== undefined && name !== null) { sets.push('name = ?'); params.push(name); }
|
||||||
if (role !== undefined && role !== null) { sets.push('role = ?'); params.push(role); }
|
if (role !== undefined && role !== null) { sets.push('role = ?'); params.push(role); }
|
||||||
if (storage_quota !== undefined && storage_quota !== null) { sets.push('storage_quota = ?'); params.push(storage_quota); }
|
if (password !== undefined && password !== null) {
|
||||||
if (password !== undefined && password !== null) { sets.push('password_hash = ?'); params.push(hashPassword(password)); }
|
sets.push('password_hash = ?');
|
||||||
|
params.push(hashPassword(password));
|
||||||
|
try {
|
||||||
|
updateSambaPassword(user.username, password);
|
||||||
|
} catch (sysErr) {
|
||||||
|
console.error('Failed to update Samba password:', sysErr.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (sets.length > 0) {
|
if (sets.length > 0) {
|
||||||
params.push(userId);
|
params.push(userId);
|
||||||
@@ -591,6 +637,13 @@ app.delete('/api/users/:userId', requireAuth, requireAdmin, (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sqlDb.prepare('DELETE FROM users WHERE id = ?').run(userId);
|
sqlDb.prepare('DELETE FROM users WHERE id = ?').run(userId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
deleteSystemUser(user.username);
|
||||||
|
} catch (sysErr) {
|
||||||
|
console.error('Failed to delete system user:', sysErr.message);
|
||||||
|
}
|
||||||
|
|
||||||
logActivity(req.user.id, 'delete', 'user', user.username);
|
logActivity(req.user.id, 'delete', 'user', user.username);
|
||||||
|
|
||||||
return res.json({ message: 'User deleted successfully' });
|
return res.json({ message: 'User deleted successfully' });
|
||||||
@@ -606,10 +659,11 @@ app.delete('/api/users/:userId', requireAuth, requireAdmin, (req, res) => {
|
|||||||
app.get('/api/files', requireAuth, (req, res) => {
|
app.get('/api/files', requireAuth, (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { parent_id, search } = req.query;
|
const { parent_id, search } = req.query;
|
||||||
|
const userDir = getUserDir(req.user.username);
|
||||||
|
|
||||||
// Search mode
|
// Search mode
|
||||||
if (search) {
|
if (search) {
|
||||||
const results = searchFiles(UPLOAD_DIR, search, '');
|
const results = searchFiles(userDir, search, '', userDir);
|
||||||
results.sort((a, b) => {
|
results.sort((a, b) => {
|
||||||
if (a.is_folder !== b.is_folder) return a.is_folder ? -1 : 1;
|
if (a.is_folder !== b.is_folder) return a.is_folder ? -1 : 1;
|
||||||
return a.name.localeCompare(b.name);
|
return a.name.localeCompare(b.name);
|
||||||
@@ -621,15 +675,15 @@ app.get('/api/files', requireAuth, (req, res) => {
|
|||||||
let relDir = '';
|
let relDir = '';
|
||||||
if (parent_id) {
|
if (parent_id) {
|
||||||
relDir = decodeFileId(parent_id);
|
relDir = decodeFileId(parent_id);
|
||||||
const safe = safeRelativePath(relDir);
|
const safe = safeRelativePath(userDir, relDir);
|
||||||
if (safe === null) {
|
if (safe === null) {
|
||||||
return res.status(400).json({ detail: 'Invalid path' });
|
return res.status(400).json({ detail: 'Invalid path' });
|
||||||
}
|
}
|
||||||
relDir = safe;
|
relDir = safe;
|
||||||
}
|
}
|
||||||
|
|
||||||
const absDir = path.join(UPLOAD_DIR, relDir);
|
const absDir = path.join(userDir, relDir);
|
||||||
if (!absDir.startsWith(UPLOAD_DIR)) {
|
if (!absDir.startsWith(userDir)) {
|
||||||
return res.status(400).json({ detail: 'Invalid path' });
|
return res.status(400).json({ detail: 'Invalid path' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -645,7 +699,7 @@ app.get('/api/files', requireAuth, (req, res) => {
|
|||||||
const fullPath = path.join(absDir, entry.name);
|
const fullPath = path.join(absDir, entry.name);
|
||||||
try {
|
try {
|
||||||
const stat = fs.statSync(fullPath);
|
const stat = fs.statSync(fullPath);
|
||||||
result.push(formatFileResponse(relativePath, stat, relDir || null));
|
result.push(formatFileResponse(userDir, relativePath, stat, relDir || null));
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
// skip files we cannot stat
|
// skip files we cannot stat
|
||||||
}
|
}
|
||||||
@@ -667,20 +721,21 @@ app.get('/api/files', requireAuth, (req, res) => {
|
|||||||
app.get('/api/files/:fileId', requireAuth, (req, res) => {
|
app.get('/api/files/:fileId', requireAuth, (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { fileId } = req.params;
|
const { fileId } = req.params;
|
||||||
|
const userDir = getUserDir(req.user.username);
|
||||||
const relPath = decodeFileId(fileId);
|
const relPath = decodeFileId(fileId);
|
||||||
const safe = safeRelativePath(relPath);
|
const safe = safeRelativePath(userDir, relPath);
|
||||||
if (safe === null) {
|
if (safe === null) {
|
||||||
return res.status(400).json({ detail: 'Invalid path' });
|
return res.status(400).json({ detail: 'Invalid path' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const absPath = path.join(UPLOAD_DIR, safe);
|
const absPath = path.join(userDir, safe);
|
||||||
if (!fs.existsSync(absPath)) {
|
if (!fs.existsSync(absPath)) {
|
||||||
return res.status(404).json({ detail: 'File not found' });
|
return res.status(404).json({ detail: 'File not found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const stat = fs.statSync(absPath);
|
const stat = fs.statSync(absPath);
|
||||||
const parentRel = path.dirname(safe);
|
const parentRel = path.dirname(safe);
|
||||||
return res.json(formatFileResponse(safe, stat, parentRel === '.' ? null : parentRel));
|
return res.json(formatFileResponse(userDir, safe, stat, parentRel === '.' ? null : parentRel));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Get file error:', err.message);
|
console.error('Get file error:', err.message);
|
||||||
return res.status(500).json({ detail: 'Internal server error' });
|
return res.status(500).json({ detail: 'Internal server error' });
|
||||||
@@ -690,6 +745,7 @@ app.get('/api/files/:fileId', requireAuth, (req, res) => {
|
|||||||
app.post('/api/files/folder', requireAuth, (req, res) => {
|
app.post('/api/files/folder', requireAuth, (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { name, parent_id } = req.body;
|
const { name, parent_id } = req.body;
|
||||||
|
const userDir = getUserDir(req.user.username);
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return res.status(400).json({ detail: 'Folder name is required' });
|
return res.status(400).json({ detail: 'Folder name is required' });
|
||||||
}
|
}
|
||||||
@@ -702,22 +758,22 @@ app.post('/api/files/folder', requireAuth, (req, res) => {
|
|||||||
let parentRel = '';
|
let parentRel = '';
|
||||||
if (parent_id) {
|
if (parent_id) {
|
||||||
parentRel = decodeFileId(parent_id);
|
parentRel = decodeFileId(parent_id);
|
||||||
const safe = safeRelativePath(parentRel);
|
const safe = safeRelativePath(userDir, parentRel);
|
||||||
if (safe === null) {
|
if (safe === null) {
|
||||||
return res.status(400).json({ detail: 'Invalid parent path' });
|
return res.status(400).json({ detail: 'Invalid parent path' });
|
||||||
}
|
}
|
||||||
parentRel = safe;
|
parentRel = safe;
|
||||||
const parentAbs = path.join(UPLOAD_DIR, parentRel);
|
const parentAbs = path.join(userDir, parentRel);
|
||||||
if (!fs.existsSync(parentAbs) || !fs.statSync(parentAbs).isDirectory()) {
|
if (!fs.existsSync(parentAbs) || !fs.statSync(parentAbs).isDirectory()) {
|
||||||
return res.status(404).json({ detail: 'Parent folder not found' });
|
return res.status(404).json({ detail: 'Parent folder not found' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newRelPath = parentRel ? path.join(parentRel, name) : name;
|
const newRelPath = parentRel ? path.join(parentRel, name) : name;
|
||||||
const newAbsPath = path.join(UPLOAD_DIR, newRelPath);
|
const newAbsPath = path.join(userDir, newRelPath);
|
||||||
|
|
||||||
// Security check
|
// Security check
|
||||||
if (!newAbsPath.startsWith(UPLOAD_DIR)) {
|
if (!newAbsPath.startsWith(userDir)) {
|
||||||
return res.status(400).json({ detail: 'Invalid path' });
|
return res.status(400).json({ detail: 'Invalid path' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -743,37 +799,30 @@ app.post('/api/files/upload', requireAuth, upload.single('file'), (req, res) =>
|
|||||||
return res.status(400).json({ detail: 'No file provided' });
|
return res.status(400).json({ detail: 'No file provided' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userDir = getUserDir(req.user.username);
|
||||||
const parentId = req.body.parent_id || null;
|
const parentId = req.body.parent_id || null;
|
||||||
|
|
||||||
// Check storage quota
|
|
||||||
const storageUsed = getUserStorageUsed(req.user.id);
|
|
||||||
const storageQuota = req.user.storage_quota || 10737418240;
|
|
||||||
const fileSize = req.file.size;
|
|
||||||
|
|
||||||
if (storageUsed + fileSize > storageQuota) {
|
|
||||||
return res.status(400).json({ detail: 'Storage quota exceeded' });
|
|
||||||
}
|
|
||||||
|
|
||||||
let parentRel = '';
|
let parentRel = '';
|
||||||
if (parentId) {
|
if (parentId) {
|
||||||
parentRel = decodeFileId(parentId);
|
parentRel = decodeFileId(parentId);
|
||||||
const safe = safeRelativePath(parentRel);
|
const safe = safeRelativePath(userDir, parentRel);
|
||||||
if (safe === null) {
|
if (safe === null) {
|
||||||
return res.status(400).json({ detail: 'Invalid parent path' });
|
return res.status(400).json({ detail: 'Invalid parent path' });
|
||||||
}
|
}
|
||||||
parentRel = safe;
|
parentRel = safe;
|
||||||
const parentAbs = path.join(UPLOAD_DIR, parentRel);
|
const parentAbs = path.join(userDir, parentRel);
|
||||||
if (!fs.existsSync(parentAbs) || !fs.statSync(parentAbs).isDirectory()) {
|
if (!fs.existsSync(parentAbs) || !fs.statSync(parentAbs).isDirectory()) {
|
||||||
return res.status(404).json({ detail: 'Parent folder not found' });
|
return res.status(404).json({ detail: 'Parent folder not found' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileName = req.file.originalname;
|
const fileName = req.file.originalname;
|
||||||
|
const fileSize = req.file.size;
|
||||||
const newRelPath = parentRel ? path.join(parentRel, fileName) : fileName;
|
const newRelPath = parentRel ? path.join(parentRel, fileName) : fileName;
|
||||||
const newAbsPath = path.join(UPLOAD_DIR, newRelPath);
|
const newAbsPath = path.join(userDir, newRelPath);
|
||||||
|
|
||||||
// Security check
|
// Security check
|
||||||
if (!newAbsPath.startsWith(UPLOAD_DIR)) {
|
if (!newAbsPath.startsWith(userDir)) {
|
||||||
return res.status(400).json({ detail: 'Invalid path' });
|
return res.status(400).json({ detail: 'Invalid path' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -794,6 +843,7 @@ app.put('/api/files/:fileId/rename', requireAuth, (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { fileId } = req.params;
|
const { fileId } = req.params;
|
||||||
const { new_name } = req.body;
|
const { new_name } = req.body;
|
||||||
|
const userDir = getUserDir(req.user.username);
|
||||||
if (!new_name) {
|
if (!new_name) {
|
||||||
return res.status(400).json({ detail: 'new_name is required' });
|
return res.status(400).json({ detail: 'new_name is required' });
|
||||||
}
|
}
|
||||||
@@ -804,21 +854,21 @@ app.put('/api/files/:fileId/rename', requireAuth, (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const relPath = decodeFileId(fileId);
|
const relPath = decodeFileId(fileId);
|
||||||
const safe = safeRelativePath(relPath);
|
const safe = safeRelativePath(userDir, relPath);
|
||||||
if (safe === null) {
|
if (safe === null) {
|
||||||
return res.status(400).json({ detail: 'Invalid path' });
|
return res.status(400).json({ detail: 'Invalid path' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const absPath = path.join(UPLOAD_DIR, safe);
|
const absPath = path.join(userDir, safe);
|
||||||
if (!fs.existsSync(absPath)) {
|
if (!fs.existsSync(absPath)) {
|
||||||
return res.status(404).json({ detail: 'File not found' });
|
return res.status(404).json({ detail: 'File not found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentDir = path.dirname(safe);
|
const parentDir = path.dirname(safe);
|
||||||
const newRelPath = parentDir === '.' ? new_name : path.join(parentDir, new_name);
|
const newRelPath = parentDir === '.' ? new_name : path.join(parentDir, new_name);
|
||||||
const newAbsPath = path.join(UPLOAD_DIR, newRelPath);
|
const newAbsPath = path.join(userDir, newRelPath);
|
||||||
|
|
||||||
if (!newAbsPath.startsWith(UPLOAD_DIR)) {
|
if (!newAbsPath.startsWith(userDir)) {
|
||||||
return res.status(400).json({ detail: 'Invalid path' });
|
return res.status(400).json({ detail: 'Invalid path' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -854,14 +904,15 @@ app.put('/api/files/:fileId/move', requireAuth, (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { fileId } = req.params;
|
const { fileId } = req.params;
|
||||||
const { destination_id } = req.body;
|
const { destination_id } = req.body;
|
||||||
|
const userDir = getUserDir(req.user.username);
|
||||||
|
|
||||||
const relPath = decodeFileId(fileId);
|
const relPath = decodeFileId(fileId);
|
||||||
const safe = safeRelativePath(relPath);
|
const safe = safeRelativePath(userDir, relPath);
|
||||||
if (safe === null) {
|
if (safe === null) {
|
||||||
return res.status(400).json({ detail: 'Invalid path' });
|
return res.status(400).json({ detail: 'Invalid path' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const absPath = path.join(UPLOAD_DIR, safe);
|
const absPath = path.join(userDir, safe);
|
||||||
if (!fs.existsSync(absPath)) {
|
if (!fs.existsSync(absPath)) {
|
||||||
return res.status(404).json({ detail: 'File not found' });
|
return res.status(404).json({ detail: 'File not found' });
|
||||||
}
|
}
|
||||||
@@ -869,12 +920,12 @@ app.put('/api/files/:fileId/move', requireAuth, (req, res) => {
|
|||||||
let destRel = '';
|
let destRel = '';
|
||||||
if (destination_id) {
|
if (destination_id) {
|
||||||
destRel = decodeFileId(destination_id);
|
destRel = decodeFileId(destination_id);
|
||||||
const safeDest = safeRelativePath(destRel);
|
const safeDest = safeRelativePath(userDir, destRel);
|
||||||
if (safeDest === null) {
|
if (safeDest === null) {
|
||||||
return res.status(400).json({ detail: 'Invalid destination path' });
|
return res.status(400).json({ detail: 'Invalid destination path' });
|
||||||
}
|
}
|
||||||
destRel = safeDest;
|
destRel = safeDest;
|
||||||
const destAbs = path.join(UPLOAD_DIR, destRel);
|
const destAbs = path.join(userDir, destRel);
|
||||||
if (!fs.existsSync(destAbs) || !fs.statSync(destAbs).isDirectory()) {
|
if (!fs.existsSync(destAbs) || !fs.statSync(destAbs).isDirectory()) {
|
||||||
return res.status(404).json({ detail: 'Destination folder not found' });
|
return res.status(404).json({ detail: 'Destination folder not found' });
|
||||||
}
|
}
|
||||||
@@ -891,9 +942,9 @@ app.put('/api/files/:fileId/move', requireAuth, (req, res) => {
|
|||||||
|
|
||||||
const fileName = path.basename(safe);
|
const fileName = path.basename(safe);
|
||||||
const newRelPath = destRel ? path.join(destRel, fileName) : fileName;
|
const newRelPath = destRel ? path.join(destRel, fileName) : fileName;
|
||||||
const newAbsPath = path.join(UPLOAD_DIR, newRelPath);
|
const newAbsPath = path.join(userDir, newRelPath);
|
||||||
|
|
||||||
if (!newAbsPath.startsWith(UPLOAD_DIR)) {
|
if (!newAbsPath.startsWith(userDir)) {
|
||||||
return res.status(400).json({ detail: 'Invalid path' });
|
return res.status(400).json({ detail: 'Invalid path' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -923,14 +974,15 @@ app.post('/api/files/:fileId/copy', requireAuth, (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { fileId } = req.params;
|
const { fileId } = req.params;
|
||||||
const { destination_id } = req.body;
|
const { destination_id } = req.body;
|
||||||
|
const userDir = getUserDir(req.user.username);
|
||||||
|
|
||||||
const relPath = decodeFileId(fileId);
|
const relPath = decodeFileId(fileId);
|
||||||
const safe = safeRelativePath(relPath);
|
const safe = safeRelativePath(userDir, relPath);
|
||||||
if (safe === null) {
|
if (safe === null) {
|
||||||
return res.status(400).json({ detail: 'Invalid path' });
|
return res.status(400).json({ detail: 'Invalid path' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const absPath = path.join(UPLOAD_DIR, safe);
|
const absPath = path.join(userDir, safe);
|
||||||
if (!fs.existsSync(absPath)) {
|
if (!fs.existsSync(absPath)) {
|
||||||
return res.status(404).json({ detail: 'File not found' });
|
return res.status(404).json({ detail: 'File not found' });
|
||||||
}
|
}
|
||||||
@@ -938,12 +990,12 @@ app.post('/api/files/:fileId/copy', requireAuth, (req, res) => {
|
|||||||
let destRel = '';
|
let destRel = '';
|
||||||
if (destination_id) {
|
if (destination_id) {
|
||||||
destRel = decodeFileId(destination_id);
|
destRel = decodeFileId(destination_id);
|
||||||
const safeDest = safeRelativePath(destRel);
|
const safeDest = safeRelativePath(userDir, destRel);
|
||||||
if (safeDest === null) {
|
if (safeDest === null) {
|
||||||
return res.status(400).json({ detail: 'Invalid destination path' });
|
return res.status(400).json({ detail: 'Invalid destination path' });
|
||||||
}
|
}
|
||||||
destRel = safeDest;
|
destRel = safeDest;
|
||||||
const destAbs = path.join(UPLOAD_DIR, destRel);
|
const destAbs = path.join(userDir, destRel);
|
||||||
if (!fs.existsSync(destAbs) || !fs.statSync(destAbs).isDirectory()) {
|
if (!fs.existsSync(destAbs) || !fs.statSync(destAbs).isDirectory()) {
|
||||||
return res.status(404).json({ detail: 'Destination folder not found' });
|
return res.status(404).json({ detail: 'Destination folder not found' });
|
||||||
}
|
}
|
||||||
@@ -951,18 +1003,11 @@ app.post('/api/files/:fileId/copy', requireAuth, (req, res) => {
|
|||||||
|
|
||||||
const stat = fs.statSync(absPath);
|
const stat = fs.statSync(absPath);
|
||||||
|
|
||||||
// Check storage quota
|
|
||||||
const fileSize = stat.isDirectory() ? getDirSize(absPath) : stat.size;
|
|
||||||
const storageUsed = getUserStorageUsed(req.user.id);
|
|
||||||
if (storageUsed + fileSize > (req.user.storage_quota || 10737418240)) {
|
|
||||||
return res.status(400).json({ detail: 'Storage quota exceeded' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileName = path.basename(safe);
|
const fileName = path.basename(safe);
|
||||||
const newRelPath = destRel ? path.join(destRel, fileName) : fileName;
|
const newRelPath = destRel ? path.join(destRel, fileName) : fileName;
|
||||||
const newAbsPath = path.join(UPLOAD_DIR, newRelPath);
|
const newAbsPath = path.join(userDir, newRelPath);
|
||||||
|
|
||||||
if (!newAbsPath.startsWith(UPLOAD_DIR)) {
|
if (!newAbsPath.startsWith(userDir)) {
|
||||||
return res.status(400).json({ detail: 'Invalid path' });
|
return res.status(400).json({ detail: 'Invalid path' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -977,7 +1022,7 @@ app.post('/api/files/:fileId/copy', requireAuth, (req, res) => {
|
|||||||
while (fs.existsSync(finalAbsPath)) {
|
while (fs.existsSync(finalAbsPath)) {
|
||||||
const newName = `${base} (${counter})${ext}`;
|
const newName = `${base} (${counter})${ext}`;
|
||||||
finalRelPath = parentDir ? path.join(parentDir, newName) : newName;
|
finalRelPath = parentDir ? path.join(parentDir, newName) : newName;
|
||||||
finalAbsPath = path.join(UPLOAD_DIR, finalRelPath);
|
finalAbsPath = path.join(userDir, finalRelPath);
|
||||||
counter++;
|
counter++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -997,14 +1042,15 @@ app.post('/api/files/:fileId/copy', requireAuth, (req, res) => {
|
|||||||
app.delete('/api/files/:fileId', requireAuth, (req, res) => {
|
app.delete('/api/files/:fileId', requireAuth, (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { fileId } = req.params;
|
const { fileId } = req.params;
|
||||||
|
const userDir = getUserDir(req.user.username);
|
||||||
|
|
||||||
const relPath = decodeFileId(fileId);
|
const relPath = decodeFileId(fileId);
|
||||||
const safe = safeRelativePath(relPath);
|
const safe = safeRelativePath(userDir, relPath);
|
||||||
if (safe === null) {
|
if (safe === null) {
|
||||||
return res.status(400).json({ detail: 'Invalid path' });
|
return res.status(400).json({ detail: 'Invalid path' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const absPath = path.join(UPLOAD_DIR, safe);
|
const absPath = path.join(userDir, safe);
|
||||||
if (!fs.existsSync(absPath)) {
|
if (!fs.existsSync(absPath)) {
|
||||||
return res.status(404).json({ detail: 'File not found' });
|
return res.status(404).json({ detail: 'File not found' });
|
||||||
}
|
}
|
||||||
@@ -1030,6 +1076,7 @@ app.delete('/api/files/:fileId', requireAuth, (req, res) => {
|
|||||||
app.post('/api/files/bulk-delete', requireAuth, (req, res) => {
|
app.post('/api/files/bulk-delete', requireAuth, (req, res) => {
|
||||||
try {
|
try {
|
||||||
const fileIds = req.body;
|
const fileIds = req.body;
|
||||||
|
const userDir = getUserDir(req.user.username);
|
||||||
if (!Array.isArray(fileIds)) {
|
if (!Array.isArray(fileIds)) {
|
||||||
return res.status(400).json({ detail: 'Request body must be an array of file IDs' });
|
return res.status(400).json({ detail: 'Request body must be an array of file IDs' });
|
||||||
}
|
}
|
||||||
@@ -1037,10 +1084,10 @@ app.post('/api/files/bulk-delete', requireAuth, (req, res) => {
|
|||||||
let deleted = 0;
|
let deleted = 0;
|
||||||
for (const fileId of fileIds) {
|
for (const fileId of fileIds) {
|
||||||
const relPath = decodeFileId(fileId);
|
const relPath = decodeFileId(fileId);
|
||||||
const safe = safeRelativePath(relPath);
|
const safe = safeRelativePath(userDir, relPath);
|
||||||
if (safe === null) continue;
|
if (safe === null) continue;
|
||||||
|
|
||||||
const absPath = path.join(UPLOAD_DIR, safe);
|
const absPath = path.join(userDir, safe);
|
||||||
if (fs.existsSync(absPath)) {
|
if (fs.existsSync(absPath)) {
|
||||||
fs.rmSync(absPath, { recursive: true, force: true });
|
fs.rmSync(absPath, { recursive: true, force: true });
|
||||||
sqlDb.prepare('DELETE FROM shares WHERE file_path = ? OR file_path LIKE ?').run(safe, safe + '/%');
|
sqlDb.prepare('DELETE FROM shares WHERE file_path = ? OR file_path LIKE ?').run(safe, safe + '/%');
|
||||||
@@ -1063,14 +1110,15 @@ app.post('/api/files/bulk-delete', requireAuth, (req, res) => {
|
|||||||
app.get('/api/files/:fileId/download', requireAuth, (req, res) => {
|
app.get('/api/files/:fileId/download', requireAuth, (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { fileId } = req.params;
|
const { fileId } = req.params;
|
||||||
|
const userDir = getUserDir(req.user.username);
|
||||||
|
|
||||||
const relPath = decodeFileId(fileId);
|
const relPath = decodeFileId(fileId);
|
||||||
const safe = safeRelativePath(relPath);
|
const safe = safeRelativePath(userDir, relPath);
|
||||||
if (safe === null) {
|
if (safe === null) {
|
||||||
return res.status(400).json({ detail: 'Invalid path' });
|
return res.status(400).json({ detail: 'Invalid path' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const absPath = path.join(UPLOAD_DIR, safe);
|
const absPath = path.join(userDir, safe);
|
||||||
if (!fs.existsSync(absPath)) {
|
if (!fs.existsSync(absPath)) {
|
||||||
return res.status(404).json({ detail: 'File not found' });
|
return res.status(404).json({ detail: 'File not found' });
|
||||||
}
|
}
|
||||||
@@ -1098,14 +1146,15 @@ app.get('/api/files/:fileId/download', requireAuth, (req, res) => {
|
|||||||
app.get('/api/files/:fileId/preview', requireAuth, (req, res) => {
|
app.get('/api/files/:fileId/preview', requireAuth, (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { fileId } = req.params;
|
const { fileId } = req.params;
|
||||||
|
const userDir = getUserDir(req.user.username);
|
||||||
|
|
||||||
const relPath = decodeFileId(fileId);
|
const relPath = decodeFileId(fileId);
|
||||||
const safe = safeRelativePath(relPath);
|
const safe = safeRelativePath(userDir, relPath);
|
||||||
if (safe === null) {
|
if (safe === null) {
|
||||||
return res.status(400).json({ detail: 'Invalid path' });
|
return res.status(400).json({ detail: 'Invalid path' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const absPath = path.join(UPLOAD_DIR, safe);
|
const absPath = path.join(userDir, safe);
|
||||||
if (!fs.existsSync(absPath)) {
|
if (!fs.existsSync(absPath)) {
|
||||||
return res.status(404).json({ detail: 'File not found' });
|
return res.status(404).json({ detail: 'File not found' });
|
||||||
}
|
}
|
||||||
@@ -1134,9 +1183,10 @@ app.get('/api/files/:fileId/preview', requireAuth, (req, res) => {
|
|||||||
app.get('/api/files/:fileId/breadcrumb', requireAuth, (req, res) => {
|
app.get('/api/files/:fileId/breadcrumb', requireAuth, (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { fileId } = req.params;
|
const { fileId } = req.params;
|
||||||
|
const userDir = getUserDir(req.user.username);
|
||||||
|
|
||||||
const relPath = decodeFileId(fileId);
|
const relPath = decodeFileId(fileId);
|
||||||
const safe = safeRelativePath(relPath);
|
const safe = safeRelativePath(userDir, relPath);
|
||||||
if (safe === null) {
|
if (safe === null) {
|
||||||
return res.status(400).json({ detail: 'Invalid path' });
|
return res.status(400).json({ detail: 'Invalid path' });
|
||||||
}
|
}
|
||||||
@@ -1167,18 +1217,19 @@ app.get('/api/files/:fileId/breadcrumb', requireAuth, (req, res) => {
|
|||||||
app.post('/api/shares', requireAuth, (req, res) => {
|
app.post('/api/shares', requireAuth, (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { file_id, expires_in_hours, password } = req.body;
|
const { file_id, expires_in_hours, password } = req.body;
|
||||||
|
const userDir = getUserDir(req.user.username);
|
||||||
if (!file_id) {
|
if (!file_id) {
|
||||||
return res.status(400).json({ detail: 'file_id is required' });
|
return res.status(400).json({ detail: 'file_id is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode file_id to a relative path
|
// Decode file_id to a relative path
|
||||||
const relPath = decodeFileId(file_id);
|
const relPath = decodeFileId(file_id);
|
||||||
const safe = safeRelativePath(relPath);
|
const safe = safeRelativePath(userDir, relPath);
|
||||||
if (safe === null) {
|
if (safe === null) {
|
||||||
return res.status(400).json({ detail: 'Invalid file path' });
|
return res.status(400).json({ detail: 'Invalid file path' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const absPath = path.join(UPLOAD_DIR, safe);
|
const absPath = path.join(userDir, safe);
|
||||||
if (!fs.existsSync(absPath)) {
|
if (!fs.existsSync(absPath)) {
|
||||||
return res.status(404).json({ detail: 'File not found' });
|
return res.status(404).json({ detail: 'File not found' });
|
||||||
}
|
}
|
||||||
@@ -1216,10 +1267,11 @@ app.post('/api/shares', requireAuth, (req, res) => {
|
|||||||
app.get('/api/shares', requireAuth, (req, res) => {
|
app.get('/api/shares', requireAuth, (req, res) => {
|
||||||
try {
|
try {
|
||||||
const shares = sqlDb.prepare('SELECT * FROM shares WHERE owner_id = ?').all(req.user.id);
|
const shares = sqlDb.prepare('SELECT * FROM shares WHERE owner_id = ?').all(req.user.id);
|
||||||
|
const userDir = getUserDir(req.user.username);
|
||||||
const result = [];
|
const result = [];
|
||||||
|
|
||||||
for (const share of shares) {
|
for (const share of shares) {
|
||||||
const absPath = path.join(UPLOAD_DIR, share.file_path);
|
const absPath = path.join(userDir, share.file_path);
|
||||||
if (fs.existsSync(absPath)) {
|
if (fs.existsSync(absPath)) {
|
||||||
const fileName = path.basename(share.file_path);
|
const fileName = path.basename(share.file_path);
|
||||||
result.push({
|
result.push({
|
||||||
@@ -1270,7 +1322,13 @@ app.get('/api/share/:token', (req, res) => {
|
|||||||
return res.status(410).json({ detail: 'Share link has expired' });
|
return res.status(410).json({ detail: 'Share link has expired' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const absPath = path.join(UPLOAD_DIR, share.file_path);
|
const owner = sqlDb.prepare('SELECT username FROM users WHERE id = ?').get(share.owner_id);
|
||||||
|
if (!owner) {
|
||||||
|
return res.status(404).json({ detail: 'Share owner not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const ownerDir = getUserDir(owner.username);
|
||||||
|
const absPath = path.join(ownerDir, share.file_path);
|
||||||
if (!fs.existsSync(absPath)) {
|
if (!fs.existsSync(absPath)) {
|
||||||
return res.status(404).json({ detail: 'File not found' });
|
return res.status(404).json({ detail: 'File not found' });
|
||||||
}
|
}
|
||||||
@@ -1312,8 +1370,14 @@ app.post('/api/share/:token/download', (req, res) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const absPath = path.join(UPLOAD_DIR, share.file_path);
|
const owner = sqlDb.prepare('SELECT username FROM users WHERE id = ?').get(share.owner_id);
|
||||||
if (!absPath.startsWith(UPLOAD_DIR) || !fs.existsSync(absPath)) {
|
if (!owner) {
|
||||||
|
return res.status(404).json({ detail: 'Share owner not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const ownerDir = getUserDir(owner.username);
|
||||||
|
const absPath = path.join(ownerDir, share.file_path);
|
||||||
|
if (!absPath.startsWith(ownerDir) || !fs.existsSync(absPath)) {
|
||||||
return res.status(404).json({ detail: 'File not found on storage' });
|
return res.status(404).json({ detail: 'File not found on storage' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1375,10 +1439,10 @@ app.get('/api/activity', requireAuth, (req, res) => {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
app.get('/api/stats', requireAuth, (req, res) => {
|
app.get('/api/stats', requireAuth, (req, res) => {
|
||||||
try {
|
try {
|
||||||
const storageUsed = getUserStorageUsed(req.user.id);
|
const userDir = getUserDir(req.user.username);
|
||||||
const storageQuota = req.user.storage_quota || 10737418240;
|
const storageUsed = getUserStorageUsed(req.user.username);
|
||||||
|
|
||||||
const counts = countFilesAndFolders(UPLOAD_DIR);
|
const counts = countFilesAndFolders(userDir);
|
||||||
const totalFiles = counts.files;
|
const totalFiles = counts.files;
|
||||||
const totalFolders = counts.folders;
|
const totalFolders = counts.folders;
|
||||||
|
|
||||||
@@ -1386,12 +1450,10 @@ app.get('/api/stats', requireAuth, (req, res) => {
|
|||||||
'SELECT COUNT(*) as count FROM shares WHERE owner_id = ? AND expires_at > ?'
|
'SELECT COUNT(*) as count FROM shares WHERE owner_id = ? AND expires_at > ?'
|
||||||
).get(req.user.id, new Date().toISOString()).count;
|
).get(req.user.id, new Date().toISOString()).count;
|
||||||
|
|
||||||
const typeBreakdown = getTypeBreakdown(UPLOAD_DIR);
|
const typeBreakdown = getTypeBreakdown(userDir);
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
storage_used: storageUsed,
|
storage_used: storageUsed,
|
||||||
storage_quota: storageQuota,
|
|
||||||
storage_percentage: storageQuota > 0 ? Math.round((storageUsed / storageQuota) * 1000) / 10 : 0,
|
|
||||||
total_files: totalFiles,
|
total_files: totalFiles,
|
||||||
total_folders: totalFolders,
|
total_folders: totalFolders,
|
||||||
active_shares: activeShares,
|
active_shares: activeShares,
|
||||||
@@ -1409,6 +1471,7 @@ app.get('/api/stats', requireAuth, (req, res) => {
|
|||||||
app.post('/api/download-zip', requireAuth, async (req, res) => {
|
app.post('/api/download-zip', requireAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { items } = req.body;
|
const { items } = req.body;
|
||||||
|
const userDir = getUserDir(req.user.username);
|
||||||
if (!Array.isArray(items) || items.length === 0) {
|
if (!Array.isArray(items) || items.length === 0) {
|
||||||
return res.status(400).json({ detail: 'items array is required and must not be empty' });
|
return res.status(400).json({ detail: 'items array is required and must not be empty' });
|
||||||
}
|
}
|
||||||
@@ -1417,12 +1480,12 @@ app.post('/api/download-zip', requireAuth, async (req, res) => {
|
|||||||
const fileRecords = [];
|
const fileRecords = [];
|
||||||
for (const itemId of items) {
|
for (const itemId of items) {
|
||||||
const relPath = decodeFileId(itemId);
|
const relPath = decodeFileId(itemId);
|
||||||
const safe = safeRelativePath(relPath);
|
const safe = safeRelativePath(userDir, relPath);
|
||||||
if (safe === null) {
|
if (safe === null) {
|
||||||
return res.status(400).json({ detail: `Invalid path for item: ${itemId}` });
|
return res.status(400).json({ detail: `Invalid path for item: ${itemId}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
const absPath = path.join(UPLOAD_DIR, safe);
|
const absPath = path.join(userDir, safe);
|
||||||
if (!fs.existsSync(absPath)) {
|
if (!fs.existsSync(absPath)) {
|
||||||
return res.status(404).json({ detail: `Item not found: ${itemId}` });
|
return res.status(404).json({ detail: `Item not found: ${itemId}` });
|
||||||
}
|
}
|
||||||
@@ -1593,6 +1656,7 @@ function seedAdmin() {
|
|||||||
'INSERT INTO users (id, username, password_hash, name, role, storage_quota, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
'INSERT INTO users (id, username, password_hash, name, role, storage_quota, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
||||||
).run(adminId, ADMIN_USERNAME, hashPassword(ADMIN_PASSWORD), 'Administrator', 'admin', 107374182400, new Date().toISOString());
|
).run(adminId, ADMIN_USERNAME, hashPassword(ADMIN_PASSWORD), 'Administrator', 'admin', 107374182400, new Date().toISOString());
|
||||||
console.log(`Admin user created: ${ADMIN_USERNAME}`);
|
console.log(`Admin user created: ${ADMIN_USERNAME}`);
|
||||||
|
createSystemUser(ADMIN_USERNAME, ADMIN_PASSWORD);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1601,8 +1665,8 @@ function seedAdmin() {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
function main() {
|
function main() {
|
||||||
try {
|
try {
|
||||||
// Ensure upload directory exists
|
// Ensure home base directory exists
|
||||||
fs.mkdirSync(UPLOAD_DIR, { recursive: true });
|
fs.mkdirSync(HOME_BASE, { recursive: true });
|
||||||
|
|
||||||
// Initialize SQLite database
|
// Initialize SQLite database
|
||||||
initDatabase();
|
initDatabase();
|
||||||
@@ -1611,6 +1675,15 @@ function main() {
|
|||||||
// Seed admin
|
// Seed admin
|
||||||
seedAdmin();
|
seedAdmin();
|
||||||
|
|
||||||
|
// Ensure existing users have home directories (migration support)
|
||||||
|
const existingUsers = sqlDb.prepare('SELECT username FROM users').all();
|
||||||
|
for (const u of existingUsers) {
|
||||||
|
const homeDir = path.join(HOME_BASE, u.username);
|
||||||
|
if (!fs.existsSync(homeDir)) {
|
||||||
|
fs.mkdirSync(homeDir, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure tmp directory exists
|
// Ensure tmp directory exists
|
||||||
fs.mkdirSync(TMP_DIR, { recursive: true });
|
fs.mkdirSync(TMP_DIR, { recursive: true });
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +1,8 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Create users group if it doesn't exist
|
|
||||||
getent group users >/dev/null 2>&1 || groupadd -g 100 users
|
|
||||||
|
|
||||||
# Create alexz user with UID 1000 if not exists
|
|
||||||
if ! id -u alexz >/dev/null 2>&1; then
|
|
||||||
# Remove any existing user with UID 1000 (e.g. ubuntu)
|
|
||||||
existing_user=$(getent passwd 1000 | cut -d: -f1)
|
|
||||||
if [ -n "$existing_user" ] && [ "$existing_user" != "alexz" ]; then
|
|
||||||
userdel "$existing_user" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
useradd -u 1000 -g users -M -s /usr/sbin/nologin alexz
|
|
||||||
echo "Created user alexz (UID 1000)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Set Samba password from environment variable
|
|
||||||
if [ -n "$SMB_PASSWORD" ]; then
|
|
||||||
echo -e "${SMB_PASSWORD}\n${SMB_PASSWORD}" | smbpasswd -a -s alexz
|
|
||||||
echo "Samba password set for alexz"
|
|
||||||
else
|
|
||||||
echo "WARNING: SMB_PASSWORD not set. Samba authentication will fail."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ensure directories exist
|
# Ensure directories exist
|
||||||
mkdir -p /config /var/log/samba /run/samba /nas
|
mkdir -p /config /var/log/samba /run/samba /nas/home
|
||||||
|
|
||||||
# Ensure correct ownership
|
|
||||||
chown alexz:users /nas 2>/dev/null || true
|
|
||||||
|
|
||||||
echo "Starting supervisord..."
|
echo "Starting supervisord..."
|
||||||
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
|
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
|||||||
@@ -9,10 +9,11 @@
|
|||||||
server min protocol = SMB2
|
server min protocol = SMB2
|
||||||
server max protocol = SMB3
|
server max protocol = SMB3
|
||||||
|
|
||||||
[data]
|
[homes]
|
||||||
path = /nas
|
comment = Home Directory
|
||||||
valid users = alexz
|
browseable = no
|
||||||
browsable = yes
|
read only = no
|
||||||
writable = yes
|
valid users = %S
|
||||||
|
path = /nas/home/%S
|
||||||
create mask = 0664
|
create mask = 0664
|
||||||
directory mask = 0775
|
directory mask = 0775
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ directory=/app
|
|||||||
autostart=true
|
autostart=true
|
||||||
autorestart=true
|
autorestart=true
|
||||||
priority=20
|
priority=20
|
||||||
environment=PORT="8080",UPLOAD_DIR="/nas",DB_PATH="/config/cloudsync.db",JWT_SECRET="%(ENV_JWT_SECRET)s",SMB_PASSWORD="%(ENV_SMB_PASSWORD)s"
|
environment=PORT="8080",DB_PATH="/config/cloudsync.db",JWT_SECRET="%(ENV_JWT_SECRET)s",SMB_PASSWORD="%(ENV_SMB_PASSWORD)s"
|
||||||
stdout_logfile=/dev/stdout
|
stdout_logfile=/dev/stdout
|
||||||
stdout_logfile_maxbytes=0
|
stdout_logfile_maxbytes=0
|
||||||
stderr_logfile=/dev/stderr
|
stderr_logfile=/dev/stderr
|
||||||
|
|||||||
Reference in New Issue
Block a user