Initial commit: Cloud Control Panel

This commit is contained in:
2026-01-10 01:24:08 +07:00
commit 01d99c5054
69 changed files with 12697 additions and 0 deletions

2304
public/assets/cloud.css Executable file

File diff suppressed because it is too large Load Diff

144
public/index.php Executable file
View File

@@ -0,0 +1,144 @@
<?php
require dirname(__DIR__) . '/vendor/autoload.php';
use Din9xtrCloud\App;
use Din9xtrCloud\Container\Container;
use Din9xtrCloud\Controllers\AuthController;
use Din9xtrCloud\Controllers\DashboardController;
use Din9xtrCloud\Controllers\LicenseController;
use Din9xtrCloud\Controllers\StorageController;
use Din9xtrCloud\Controllers\StorageTusController;
use Din9xtrCloud\Middlewares\AuthMiddleware;
use Din9xtrCloud\Middlewares\CsrfMiddleware;
use Din9xtrCloud\Router;
use Din9xtrCloud\Storage\Drivers\LocalStorageDriver;
use Din9xtrCloud\Storage\Drivers\StorageDriverInterface;
use Din9xtrCloud\Storage\UserStorageInitializer;
use Din9xtrCloud\ViewModels\BaseViewModel;
use Din9xtrCloud\ViewModels\LayoutConfig;
use FastRoute\RouteCollector;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Monolog\Processor\PsrLogMessageProcessor;
use Nyholm\Psr7\Factory\Psr17Factory;
use Monolog\Level;
use Nyholm\Psr7Server\ServerRequestCreator;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
// ---------------------------------------------------------------------
// PHP runtime
// ---------------------------------------------------------------------
error_reporting(E_ALL);
session_start();
// ---------------------------------------------------------------------
// ENV
// ---------------------------------------------------------------------
$storageBasePath = dirname(__DIR__) . '/' . ($_ENV['STORAGE_PATH'] ?? 'storage');
$userLimitBytes = (int)($_ENV['STORAGE_USER_LIMIT_GB'] ?? 70) * 1024 * 1024 * 1024;
// ---------------------------------------------------------------------
// Container
// ---------------------------------------------------------------------
$container = new Container();
$logPath = dirname(__DIR__) . '/logs/cloud.log';
if (!is_dir(dirname($logPath))) mkdir(dirname($logPath), 0755, true);
$container->singleton(StorageDriverInterface::class, function () use ($storageBasePath, $userLimitBytes) {
return new LocalStorageDriver(
basePath: $storageBasePath,
defaultLimitBytes: $userLimitBytes
);
});
$container->singleton(UserStorageInitializer::class, function () use ($storageBasePath) {
return new UserStorageInitializer($storageBasePath);
});
$container->singleton(LoggerInterface::class, function () use ($logPath) {
$logger = new Logger('cloud');
$logger->pushHandler(new StreamHandler($logPath, Level::Debug));
$logger->pushProcessor(new PsrLogMessageProcessor());
return $logger;
});
$container->singleton(PDO::class, function () {
return new PDO(
'sqlite:/var/db/database.sqlite',
null,
null,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]
);
});
$container->singleton(Psr17Factory::class, fn() => new Psr17Factory());
$container->request(
ServerRequestInterface::class,
fn(Container $c) => new ServerRequestCreator(
$c->get(Psr17Factory::class),
$c->get(Psr17Factory::class),
$c->get(Psr17Factory::class),
$c->get(Psr17Factory::class),
)->fromGlobals()
);
// ---------------------------------------------------------------------
// Routes
// ---------------------------------------------------------------------
$routes = static function (RouteCollector $r): void {
$r->get('/', [DashboardController::class, 'index']);
$r->get('/license', [LicenseController::class, 'license']);
$r->get('/login', [AuthController::class, 'loginForm']);
$r->post('/login', [AuthController::class, 'loginSubmit']);
$r->post('/logout', [AuthController::class, 'logout']);
$r->post('/storage/folders', [StorageController::class, 'createFolder']);
$r->post('/storage/files', [StorageController::class, 'uploadFile']);
$r->get('/folders/{folder}', [StorageController::class, 'showFolder']);
$r->post('/storage/folders/{folder}/delete', [StorageController::class, 'deleteFolder']);
$r->addRoute(['POST', 'OPTIONS'], '/storage/tus', [
StorageTusController::class,
'handle',
]);
$r->patch('/storage/tus/{id:[a-f0-9]+}', [StorageTusController::class, 'patch']);
$r->head('/storage/tus/{id:[a-f0-9]+}', [StorageTusController::class, 'head']);
$r->get('/storage/files/download', [StorageController::class, 'downloadFile']);
$r->post('/storage/files/delete', [StorageController::class, 'deleteFile']);
$r->get('/storage/files/download/multiple', [StorageController::class, 'downloadMultiple']);
$r->post('/storage/files/delete/multiple', [StorageController::class, 'deleteMultiple']);
};
// route,middlewares
$router = new Router($routes, $container);
//$router->middlewareFor('/', AuthMiddleware::class);
//$router->middlewareFor('/login', AuthMiddleware::class);
$app = new App($container);
//global,middlewares
$app->middleware(
CsrfMiddleware::class,
AuthMiddleware::class
);
$app->router($router);
try {
$container->beginRequest();
$app->dispatch();
} catch (Throwable $e) {
http_response_code(500);
header('Content-Type: text/plain; charset=utf-8');
echo 'Internal Server Error';
}

60
public/js/dashboard.js Normal file
View File

@@ -0,0 +1,60 @@
(function () {
'use strict';
const {modalManager, FileUploader, showNotification} = window.sharedUtils;
/* =======================
* ELEMENTS
* ======================= */
const folderModal = document.getElementById('create-folder-modal');
const uploadModal = document.getElementById('upload-file-modal');
const folderOpenBtn = document.getElementById('create-folder-btn');
const uploadOpenBtn = document.getElementById('upload-file-btn');
const folderCloseBtn = document.getElementById('cancel-create-folder');
const uploadCloseBtn = document.getElementById('cancel-upload-file');
document.addEventListener('DOMContentLoaded', () => {
if (folderModal) folderModal.style.display = 'none';
if (uploadModal) uploadModal.style.display = 'none';
if (uploadModal) {
new FileUploader({
formSelector: '#upload-file-modal form',
fileInputSelector: '#upload-file-modal input[type="file"]',
folderSelectSelector: '#upload-file-modal select[name="folder"]',
progressFillSelector: '#upload-file-modal .progress-fill',
progressTextSelector: '#upload-file-modal .progress-text',
uploadProgressSelector: '#upload-file-modal .upload-progress',
submitBtnSelector: '#upload-file-modal #submit-upload',
cancelBtnSelector: '#upload-file-modal #cancel-upload-file'
});
}
if (folderOpenBtn) {
folderOpenBtn.addEventListener('click', () => modalManager.open(folderModal));
}
if (uploadOpenBtn) {
uploadOpenBtn.addEventListener('click', () => modalManager.open(uploadModal));
}
if (folderCloseBtn) {
folderCloseBtn.addEventListener('click', () => modalManager.close());
}
if (uploadCloseBtn) {
uploadCloseBtn.addEventListener('click', () => modalManager.close());
}
[folderModal, uploadModal].forEach(modal => {
if (modal) {
modal.addEventListener('click', e => {
if (e.target === modal) modalManager.close();
});
}
});
});
})();

229
public/js/folder.js Normal file
View File

@@ -0,0 +1,229 @@
(function () {
'use strict';
const {modalManager, FileUploader, showNotification} = window.sharedUtils;
/* =======================
* SELECT/DELETE FILES LOGIC
* ======================= */
class FileSelectionManager {
constructor() {
this.selectAllCheckbox = document.getElementById('select-all-checkbox');
this.fileCheckboxes = document.querySelectorAll('.file-select-checkbox');
this.fileCards = document.querySelectorAll('.file-card');
this.deleteMultipleForm = document.querySelector('.multiple-delete-form');
this.downloadMultipleForm = document.querySelector('.multiple-download-form');
this.deleteMultipleBtn = document.getElementById('delete-multiple-btn');
this.downloadMultipleBtn = document.getElementById('download-multiple-btn');
this.multipleFileNamesInput = document.getElementById('multiple-file-names');
this.multipleDownloadNamesInput = document.getElementById('multiple-download-names');
this.init();
}
init() {
if (this.selectAllCheckbox) {
this.selectAllCheckbox.addEventListener('change', () => this.toggleSelectAll());
}
this.fileCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', () => this.updateSelectionState());
});
this.fileCards.forEach(card => {
card.addEventListener('click', (e) => this.handleCardClick(e, card));
});
if (this.deleteMultipleForm) {
this.deleteMultipleForm.addEventListener('submit', (e) => this.handleMultipleDelete(e));
}
if (this.downloadMultipleForm) {
this.downloadMultipleForm.addEventListener('submit', (e) => this.handleMultipleDownload(e));
}
}
handleCardClick(e, card) {
if (e.target.closest('.file-card-actions') ||
e.target.closest('.file-action-btn') ||
e.target.closest('.delete-form') ||
e.target.closest('a')) {
return;
}
const checkbox = card.querySelector('.file-select-checkbox');
if (checkbox) {
checkbox.checked = !checkbox.checked;
checkbox.dispatchEvent(new Event('change'));
}
}
toggleSelectAll() {
const isChecked = this.selectAllCheckbox.checked;
this.fileCheckboxes.forEach(checkbox => {
checkbox.checked = isChecked;
checkbox.dispatchEvent(new Event('change'));
});
this.updateSelectionState();
}
updateSelectionState() {
const selectedFiles = Array.from(this.fileCheckboxes)
.filter(cb => cb.checked)
.map(cb => cb.value);
// flash selected
this.fileCards.forEach(card => {
const fileName = card.dataset.fileName;
if (selectedFiles.includes(fileName)) {
card.classList.add('selected');
} else {
card.classList.remove('selected');
}
});
// show/hide action-buttons
if (selectedFiles.length > 0) {
this.deleteMultipleForm.style.display = 'block';
this.downloadMultipleForm.style.display = 'inline-block';
this.multipleFileNamesInput.value = JSON.stringify(selectedFiles);
this.multipleDownloadNamesInput.value = JSON.stringify(selectedFiles);
this.deleteMultipleBtn.innerHTML = `
<span class="btn-icon">🗑️</span>
Delete Selected (${selectedFiles.length})
`;
this.downloadMultipleBtn.innerHTML = `
<span class="btn-icon">⬇️</span>
Download Selected (${selectedFiles.length})
`;
} else {
this.deleteMultipleForm.style.display = 'none';
this.downloadMultipleForm.style.display = 'none';
}
// refresh select all
if (this.selectAllCheckbox) {
this.selectAllCheckbox.checked = selectedFiles.length === this.fileCheckboxes.length && this.fileCheckboxes.length > 0;
this.selectAllCheckbox.indeterminate = selectedFiles.length > 0 && selectedFiles.length < this.fileCheckboxes.length;
}
}
handleMultipleDelete(e) {
if (!confirm('Are you sure you want to delete selected files?')) {
e.preventDefault();
return false;
}
return true;
}
handleMultipleDownload(e) {
console.log('Downloading selected files...');
return true;
}
}
/* =======================
* FILE PREVIEW
* ======================= */
class FilePreviewManager {
constructor(fileInput, previewContainer) {
this.fileInput = fileInput;
this.previewContainer = previewContainer;
this.img = previewContainer.querySelector('img');
this.info = previewContainer.querySelector('.file-info');
this.init();
}
init() {
this.fileInput.addEventListener('change', () => this.updatePreview());
}
updatePreview() {
const file = this.fileInput.files[0];
if (!file) {
this.previewContainer.style.display = 'none';
return;
}
this.previewContainer.style.display = 'block';
this.info.innerHTML = `
<div><strong>Name:</strong> ${file.name}</div>
<div><strong>Size:</strong> ${formatBytes(file.size)}</div>
<div><strong>Type:</strong> ${file.type || 'Unknown'}</div>
`;
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
this.img.src = e.target.result;
this.img.style.display = 'block';
};
reader.readAsDataURL(file);
} else {
this.img.style.display = 'none';
}
}
reset() {
this.previewContainer.style.display = 'none';
this.img.src = '';
this.img.style.display = 'none';
this.info.innerHTML = '';
}
}
/* =======================
* Load init
* ======================= */
document.addEventListener('DOMContentLoaded', () => {
const uploadModal = document.getElementById('upload-file-modal-folder');
if (uploadModal) {
uploadModal.style.display = 'none';
new FileUploader({
formSelector: '#upload-form-folder',
fileInputSelector: '#file-input-folder',
progressFillSelector: '#upload-file-modal-folder .progress-fill',
progressTextSelector: '#upload-file-modal-folder .progress-text',
uploadProgressSelector: '#upload-file-modal-folder .upload-progress',
submitBtnSelector: '#upload-file-modal-folder #submit-upload-folder',
cancelBtnSelector: '#upload-file-modal-folder #cancel-upload-file-folder',
onSuccess: () => {
showNotification('File uploaded successfully', 'success');
setTimeout(() => location.reload(), 1000);
}
});
const fileInput = document.getElementById('file-input-folder');
const previewContainer = uploadModal.querySelector('.file-preview');
if (fileInput && previewContainer) {
new FilePreviewManager(fileInput, previewContainer);
}
}
if (document.querySelector('.file-select-checkbox')) {
new FileSelectionManager();
}
const uploadBtn = document.getElementById('upload-file-folder');
const uploadFirstBtn = document.getElementById('upload-first-file');
const cancelUploadBtn = document.getElementById('cancel-upload-file-folder');
const openUploadModal = () => modalManager.open(uploadModal);
const closeUploadModal = () => modalManager.close();
if (uploadBtn) uploadBtn.addEventListener('click', openUploadModal);
if (uploadFirstBtn) uploadFirstBtn.addEventListener('click', openUploadModal);
if (cancelUploadBtn) cancelUploadBtn.addEventListener('click', closeUploadModal);
if (uploadModal) {
uploadModal.addEventListener('click', e => {
if (e.target === uploadModal) closeUploadModal();
});
}
});
})();

363
public/js/shared.js Normal file
View File

@@ -0,0 +1,363 @@
// /js/shared.js
(function () {
'use strict';
/* =======================
* CONFIG
* ======================= */
const SMALL_FILE_LIMIT = 256 * 1024 * 1024; // 256MB
const TUS_ENDPOINT = '/storage/tus';
/* =======================
* STATE
* ======================= */
let currentUploadType = '';
/* =======================
* UTILS
* ======================= */
function formatBytes(bytes) {
if (!bytes) return '0 Bytes';
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i];
}
function getErrorMessage(code) {
const errors = {
no_file: 'No file selected',
upload_failed: 'Upload failed',
storage_limit: 'Storage limit exceeded',
invalid_folder: 'Invalid folder selected'
};
return errors[code] || 'Unknown error';
}
function getSuccessMessage(code) {
const success = {
file_uploaded: 'File uploaded successfully',
folder_created: 'Folder created successfully',
files_deleted: 'Files deleted successfully'
};
return success[code] || 'Success';
}
/* =======================
* NOTIFICATIONS
* ======================= */
window.showNotification = function (message, type = 'info') {
const body = document.body;
const notification = document.createElement('div');
const oldNotifications = document.querySelectorAll('.global-notification');
oldNotifications.forEach(n => n.remove());
notification.className = 'global-notification';
notification.textContent = message;
const bgColor = type === 'error' ? '#e53e3e' :
type === 'success' ? '#38a169' :
type === 'warning' ? '#d69e2e' : '#3182ce';
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 12px 16px;
background: ${bgColor};
color: #fff;
border-radius: 8px;
z-index: 10000;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
animation: slideIn 0.3s ease-out;
`;
body.appendChild(notification);
setTimeout(() => {
notification.style.animation = 'slideOut 0.3s ease-in';
setTimeout(() => notification.remove(), 300);
}, 3000);
if (!document.querySelector('#notification-styles')) {
const style = document.createElement('style');
style.id = 'notification-styles';
style.textContent = `
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
`;
document.head.appendChild(style);
}
};
/* =======================
* MODAL MANAGEMENT
* ======================= */
window.modalManager = {
currentModal: null,
open(modal) {
this.closeAll();
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
this.currentModal = modal;
const escHandler = (e) => {
if (e.key === 'Escape') this.close();
};
modal._escHandler = escHandler;
document.addEventListener('keydown', escHandler);
},
close() {
if (this.currentModal) {
document.removeEventListener('keydown', this.currentModal._escHandler);
this.currentModal.style.display = 'none';
document.body.style.overflow = '';
this.currentModal = null;
}
},
closeAll() {
document.querySelectorAll('.modal-overlay').forEach(modal => {
modal.style.display = 'none';
if (modal._escHandler) {
document.removeEventListener('keydown', modal._escHandler);
}
});
document.body.style.overflow = '';
this.currentModal = null;
}
};
/* =======================
* FILE UPLOAD
* ======================= */
class FileUploader {
constructor(options = {}) {
this.options = {
formSelector: '',
fileInputSelector: '',
folderSelectSelector: null,
progressFillSelector: '.progress-fill',
progressTextSelector: '.progress-text',
uploadProgressSelector: '.upload-progress',
submitBtnSelector: '',
cancelBtnSelector: '',
onSuccess: null,
onError: null,
...options
};
this.form = document.querySelector(this.options.formSelector);
this.fileInput = document.querySelector(this.options.fileInputSelector);
this.folderSelect = this.options.folderSelectSelector ?
document.querySelector(this.options.folderSelectSelector) : null;
this.progressFill = document.querySelector(this.options.progressFillSelector);
this.progressText = document.querySelector(this.options.progressTextSelector);
this.uploadProgress = document.querySelector(this.options.uploadProgressSelector);
this.submitBtn = document.querySelector(this.options.submitBtnSelector);
this.cancelBtn = document.querySelector(this.options.cancelBtnSelector);
this.bindEvents();
}
bindEvents() {
if (this.form) {
this.form.addEventListener('submit', (e) => this.handleSubmit(e));
}
if (this.fileInput) {
this.fileInput.addEventListener('change', () => this.handleFileSelect());
}
if (this.cancelBtn) {
this.cancelBtn.addEventListener('click', () => this.reset());
}
}
handleFileSelect() {
const file = this.fileInput.files[0];
if (!file) return;
console.log('File selected:', file.name, formatBytes(file.size));
}
async handleSubmit(e) {
e.preventDefault();
const file = this.fileInput.files[0];
const folder = this.folderSelect ? this.folderSelect.value : this.form.querySelector('input[name="folder"]')?.value;
if (!file) {
showNotification('Please select a file', 'error');
return;
}
if (this.folderSelect && !folder) {
showNotification('Please select a folder', 'error');
return;
}
this.setUploadingState(true);
if (file.size <= SMALL_FILE_LIMIT) {
await this.uploadSmallFile(file, folder);
} else {
this.uploadLargeFile(file, folder);
}
}
async uploadSmallFile(file, folder) {
try {
this.updateProgress(0, 'Starting upload...');
const formData = new FormData();
formData.append('file', file);
if (folder) formData.append('folder', folder);
formData.append('_csrf', this.form.querySelector('input[name="_csrf"]')?.value || '');
let progress = 0;
const progressInterval = setInterval(() => {
if (progress < 90) {
progress += 10;
this.updateProgress(progress);
}
}, 300);
const response = await fetch('/storage/files', {
method: 'POST',
body: formData
});
clearInterval(progressInterval);
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'Upload failed');
}
this.updateProgress(100, 'Upload complete!');
this.onUploadSuccess();
} catch (error) {
this.onUploadError(error.message);
}
}
uploadLargeFile(file, folder) {
this.updateProgress(0, 'Starting large file upload...');
const upload = new tus.Upload(file, {
endpoint: TUS_ENDPOINT,
chunkSize: 5 * 1024 * 1024,
retryDelays: [0, 1000, 3000, 5000],
metadata: {
folder: folder || '',
filename: file.name
},
withCredentials: true,
onProgress: (uploaded, total) => {
const percent = Math.round((uploaded / total) * 100);
this.updateProgress(percent, `${percent}%`);
},
onSuccess: () => {
this.updateProgress(100, 'Upload complete!');
setTimeout(() => this.onUploadSuccess(), 500);
},
onError: (error) => {
this.onUploadError(error.toString());
}
});
upload.start();
}
updateProgress(percent, text = '') {
if (this.progressFill) {
this.progressFill.style.width = percent + '%';
}
if (this.progressText) {
this.progressText.textContent = text || percent + '%';
}
}
onUploadSuccess() {
showNotification('File uploaded successfully', 'success');
this.setUploadingState(false);
modalManager.close();
this.reset();
if (this.options.onSuccess) {
this.options.onSuccess();
} else {
setTimeout(() => location.reload(), 1000);
}
}
onUploadError(error) {
showNotification(error, 'error');
this.setUploadingState(false);
this.reset();
if (this.options.onError) {
this.options.onError(error);
}
}
setUploadingState(uploading) {
if (this.submitBtn) this.submitBtn.disabled = uploading;
if (this.cancelBtn) this.cancelBtn.disabled = uploading;
if (this.uploadProgress) {
this.uploadProgress.style.display = uploading ? 'block' : 'none';
}
}
reset() {
if (this.form) this.form.reset();
this.updateProgress(0);
this.setUploadingState(false);
}
}
/* =======================
* INIT URL PARAMS
* ======================= */
function initUrlParams() {
const params = new URLSearchParams(window.location.search);
if (params.get('error')) {
showNotification(getErrorMessage(params.get('error')), 'error');
}
if (params.get('success')) {
showNotification(getSuccessMessage(params.get('success')), 'success');
}
}
/* =======================
* EXPORTS
* ======================= */
window.sharedUtils = {
formatBytes,
getErrorMessage,
getSuccessMessage,
showNotification,
modalManager,
FileUploader,
initUrlParams,
SMALL_FILE_LIMIT,
TUS_ENDPOINT
};
document.addEventListener('DOMContentLoaded', initUrlParams);
})();

4989
public/js/tus.js Normal file

File diff suppressed because one or more lines are too long