Use background jobs for file transfers
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
const UNAV_DEFAULT_ROOT = '/mnt/user';
|
||||
const UNAV_JOB_DIR = '/tmp/u-navigator/jobs';
|
||||
|
||||
function unav_roots(): array {
|
||||
$configured = getenv('U_NAV_ROOTS') ?: UNAV_DEFAULT_ROOT;
|
||||
@@ -152,3 +153,35 @@ function unav_guard_destination(string $source, string $destination): void {
|
||||
unav_error(400, 'Destination cannot be inside source');
|
||||
}
|
||||
}
|
||||
|
||||
function unav_job_dir(): string {
|
||||
if (!is_dir(UNAV_JOB_DIR) && !mkdir(UNAV_JOB_DIR, 0777, true) && !is_dir(UNAV_JOB_DIR)) {
|
||||
unav_error(500, 'Could not create job directory');
|
||||
}
|
||||
return UNAV_JOB_DIR;
|
||||
}
|
||||
|
||||
function unav_job_path(string $id): string {
|
||||
if (!preg_match('/^[A-Za-z0-9._-]+$/', $id)) {
|
||||
unav_error(400, 'Invalid job id');
|
||||
}
|
||||
return unav_job_dir() . '/' . $id . '.json';
|
||||
}
|
||||
|
||||
function unav_job_read(string $id): array {
|
||||
$path = unav_job_path($id);
|
||||
if (!is_file($path)) {
|
||||
unav_error(404, 'Job not found');
|
||||
}
|
||||
$job = json_decode(file_get_contents($path) ?: '{}', true);
|
||||
if (!is_array($job)) {
|
||||
unav_error(500, 'Invalid job state');
|
||||
}
|
||||
return $job;
|
||||
}
|
||||
|
||||
function unav_job_write(array $job): void {
|
||||
$id = $job['id'] ?? '';
|
||||
$job['updatedAt'] = date(DATE_ATOM);
|
||||
file_put_contents(unav_job_path($id), json_encode($job, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT), LOCK_EX);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/common.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
unav_json(unav_job_read($_GET['id'] ?? ''));
|
||||
}
|
||||
|
||||
unav_assert_writable();
|
||||
|
||||
$action = $_GET['action'] ?? '';
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
unav_json([
|
||||
'id' => $_GET['id'] ?? '0',
|
||||
'type' => 'unknown',
|
||||
'status' => 'completed',
|
||||
'progress' => 100,
|
||||
'message' => 'Completed',
|
||||
'error' => null,
|
||||
'createdAt' => date(DATE_ATOM),
|
||||
'updatedAt' => date(DATE_ATOM),
|
||||
]);
|
||||
if (!in_array($action, ['move', 'copy'], true)) {
|
||||
unav_error(404, 'Unknown job action');
|
||||
}
|
||||
|
||||
$data = unav_request_json();
|
||||
@@ -22,24 +17,29 @@ $source = unav_existing_path($data['source'] ?? null);
|
||||
$destination = unav_destination_path($data['destination'] ?? null);
|
||||
unav_guard_destination($source['path'], $destination['path']);
|
||||
|
||||
if ($action === 'move') {
|
||||
if (!rename($source['path'], $destination['path'])) {
|
||||
unav_copy_recursive($source['path'], $destination['path']);
|
||||
unav_delete_recursive($source['path']);
|
||||
}
|
||||
} elseif ($action === 'copy') {
|
||||
unav_copy_recursive($source['path'], $destination['path']);
|
||||
$id = str_replace('.', '', uniqid($action . '-', true));
|
||||
$now = date(DATE_ATOM);
|
||||
$job = [
|
||||
'id' => $id,
|
||||
'type' => $action,
|
||||
'status' => 'queued',
|
||||
'progress' => 0,
|
||||
'message' => 'Queued',
|
||||
'error' => null,
|
||||
'source' => $source['path'],
|
||||
'destination' => $destination['path'],
|
||||
'createdAt' => $now,
|
||||
'updatedAt' => $now,
|
||||
];
|
||||
unav_job_write($job);
|
||||
|
||||
$worker = __DIR__ . '/job_worker.php';
|
||||
$php = is_executable('/usr/bin/php') ? '/usr/bin/php' : (PHP_BINARY ?: 'php');
|
||||
if (function_exists('exec') && is_file($worker)) {
|
||||
exec(escapeshellarg($php) . ' ' . escapeshellarg($worker) . ' ' . escapeshellarg($id) . ' > /dev/null 2>&1 &');
|
||||
} else {
|
||||
unav_error(404, 'Unknown job action');
|
||||
require $worker;
|
||||
unav_run_job($id);
|
||||
}
|
||||
|
||||
unav_json([
|
||||
'id' => (string)time(),
|
||||
'type' => $action,
|
||||
'status' => 'completed',
|
||||
'progress' => 100,
|
||||
'message' => 'Completed',
|
||||
'error' => null,
|
||||
'createdAt' => date(DATE_ATOM),
|
||||
'updatedAt' => date(DATE_ATOM),
|
||||
], 202);
|
||||
unav_json($job, 202);
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/common.php';
|
||||
|
||||
if (PHP_SAPI === 'cli' && realpath($_SERVER['SCRIPT_FILENAME'] ?? '') === __FILE__) {
|
||||
unav_run_job($argv[1] ?? '');
|
||||
}
|
||||
|
||||
function unav_run_job(string $id): void {
|
||||
$job = unav_job_read($id);
|
||||
try {
|
||||
unav_worker_update($job, 'running', 5, 'Preparing');
|
||||
unav_worker_guard($job['source'], $job['destination']);
|
||||
|
||||
if ($job['type'] === 'move') {
|
||||
unav_worker_move($job['source'], $job['destination'], $job);
|
||||
} elseif ($job['type'] === 'copy') {
|
||||
unav_worker_copy($job['source'], $job['destination'], $job);
|
||||
} else {
|
||||
throw new RuntimeException('Unknown job action');
|
||||
}
|
||||
|
||||
unav_worker_update($job, 'completed', 100, 'Completed');
|
||||
} catch (Throwable $error) {
|
||||
$job['error'] = $error->getMessage();
|
||||
unav_worker_update($job, 'failed', 100, $error->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
function unav_worker_update(array &$job, string $status, int $progress, string $message): void {
|
||||
$job['status'] = $status;
|
||||
$job['progress'] = $progress;
|
||||
$job['message'] = $message;
|
||||
unav_job_write($job);
|
||||
}
|
||||
|
||||
function unav_worker_guard(string $source, string $destination): void {
|
||||
$sourceReal = realpath($source);
|
||||
if ($sourceReal === false) {
|
||||
throw new RuntimeException('Source not found');
|
||||
}
|
||||
unav_worker_assert_inside_root($sourceReal);
|
||||
|
||||
$parentReal = realpath(dirname($destination));
|
||||
if ($parentReal === false) {
|
||||
throw new RuntimeException('Destination parent not found');
|
||||
}
|
||||
unav_worker_assert_inside_root($parentReal);
|
||||
|
||||
$destinationPath = $parentReal . '/' . basename($destination);
|
||||
if (file_exists($destinationPath)) {
|
||||
throw new RuntimeException('Destination already exists');
|
||||
}
|
||||
if ($sourceReal === $destinationPath || str_starts_with($destinationPath, rtrim($sourceReal, '/') . '/')) {
|
||||
throw new RuntimeException('Destination cannot be inside source');
|
||||
}
|
||||
}
|
||||
|
||||
function unav_worker_assert_inside_root(string $path): void {
|
||||
foreach (unav_roots() as $root) {
|
||||
if (unav_is_inside($path, $root['path'])) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new RuntimeException('Path is outside allowed roots');
|
||||
}
|
||||
|
||||
function unav_worker_move(string $source, string $destination, array &$job): void {
|
||||
unav_worker_update($job, 'running', 15, 'Moving');
|
||||
if (@rename($source, $destination)) {
|
||||
return;
|
||||
}
|
||||
unav_worker_copy($source, $destination, $job);
|
||||
unav_worker_delete($source);
|
||||
}
|
||||
|
||||
function unav_worker_copy(string $source, string $destination, array &$job): void {
|
||||
if (is_link($source)) {
|
||||
throw new RuntimeException('Copying symlinks is not allowed');
|
||||
}
|
||||
if (is_dir($source)) {
|
||||
if (!mkdir($destination, 0777, true) && !is_dir($destination)) {
|
||||
throw new RuntimeException('Could not create destination directory');
|
||||
}
|
||||
$entries = array_values(array_filter(scandir($source) ?: [], fn($entry) => $entry !== '.' && $entry !== '..'));
|
||||
$total = max(count($entries), 1);
|
||||
foreach ($entries as $index => $entry) {
|
||||
unav_worker_copy($source . '/' . $entry, $destination . '/' . $entry, $job);
|
||||
unav_worker_update($job, 'running', min(95, 10 + (int)((($index + 1) / $total) * 80)), 'Copying directory');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!is_file($source)) {
|
||||
throw new RuntimeException('Only files and directories can be copied');
|
||||
}
|
||||
unav_worker_update($job, 'running', 35, 'Copying file');
|
||||
if (!copy($source, $destination)) {
|
||||
throw new RuntimeException('Copy failed');
|
||||
}
|
||||
}
|
||||
|
||||
function unav_worker_delete(string $path): void {
|
||||
if (is_link($path) || is_file($path)) {
|
||||
if (!unlink($path)) {
|
||||
throw new RuntimeException('Could not remove source after move');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (is_dir($path)) {
|
||||
foreach (scandir($path) ?: [] as $entry) {
|
||||
if ($entry === '.' || $entry === '..') {
|
||||
continue;
|
||||
}
|
||||
unav_worker_delete($path . '/' . $entry);
|
||||
}
|
||||
if (!rmdir($path)) {
|
||||
throw new RuntimeException('Could not remove source directory after move');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user