Overworked files API and explorer
This commit is contained in:
37
api/copy.php
Normal file
37
api/copy.php
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/../vfs.php';
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$srcV = $data['src'] ?? '';
|
||||||
|
$destV = $data['dest'] ?? '';
|
||||||
|
|
||||||
|
$srcReal = resolve_path($srcV);
|
||||||
|
$destReal = resolve_path($destV);
|
||||||
|
|
||||||
|
if (!file_exists($srcReal) || !is_dir($destReal)) {
|
||||||
|
echo json_encode(['success'=>false, 'error'=>'Invalid paths']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$basename = basename($srcReal);
|
||||||
|
$target = $destReal . '/' . $basename;
|
||||||
|
|
||||||
|
if (is_dir($srcReal)) {
|
||||||
|
$rc = mkdir($target);
|
||||||
|
// simple recursive copy
|
||||||
|
$it = new RecursiveIteratorIterator(
|
||||||
|
new RecursiveDirectoryIterator($srcReal, FilesystemIterator::SKIP_DOTS),
|
||||||
|
RecursiveIteratorIterator::SELF_FIRST
|
||||||
|
);
|
||||||
|
foreach ($it as $item) {
|
||||||
|
$subPath = $target . substr($item->getPathname(), strlen($srcReal));
|
||||||
|
if ($item->isDir()) mkdir($subPath);
|
||||||
|
else copy($item->getPathname(), $subPath);
|
||||||
|
}
|
||||||
|
$ok = $rc;
|
||||||
|
} else {
|
||||||
|
$ok = copy($srcReal, $target);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['success'=>(bool)$ok]);
|
26
api/delete.php
Normal file
26
api/delete.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/../vfs.php';
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$pathV = $data['path'] ?? '';
|
||||||
|
|
||||||
|
$real = resolve_path($pathV);
|
||||||
|
if (!file_exists($real)) {
|
||||||
|
echo json_encode(['success'=>false,'error'=>'Not found']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rrmdir($d) {
|
||||||
|
foreach (scandir($d) as $f) {
|
||||||
|
if (in_array($f, ['.','..'])) continue;
|
||||||
|
$p = "$d/$f";
|
||||||
|
is_dir($p) ? rrmdir($p) : unlink($p);
|
||||||
|
}
|
||||||
|
rmdir($d);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_dir($real)) rrmdir($real);
|
||||||
|
else unlink($real);
|
||||||
|
|
||||||
|
echo json_encode(['success'=>true]);
|
19
api/info.php
Normal file
19
api/info.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/../vfs.php';
|
||||||
|
|
||||||
|
$pathV = $_GET['path'] ?? '';
|
||||||
|
$real = resolve_path($pathV);
|
||||||
|
|
||||||
|
if (!file_exists($real)) {
|
||||||
|
echo json_encode([]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'name' => basename($real),
|
||||||
|
'type' => is_dir($real) ? 'directory' : 'file',
|
||||||
|
'size' => is_file($real) ? filesize($real) : null,
|
||||||
|
'mtime' => date(DATE_ISO8601, filemtime($real)),
|
||||||
|
'path' => virtualize_path($real),
|
||||||
|
]);
|
30
api/list.php
Normal file
30
api/list.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/../vfs.php';
|
||||||
|
|
||||||
|
$dir = $_GET['dir'] ?? '';
|
||||||
|
$showHidden = ($_GET['hidden'] ?? '0') === '1';
|
||||||
|
|
||||||
|
$realDir = resolve_path($dir);
|
||||||
|
if (!is_dir($realDir)) {
|
||||||
|
echo json_encode([]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$out = [];
|
||||||
|
foreach (scandir($realDir) as $name) {
|
||||||
|
if ($name === '.' || $name === '..') continue;
|
||||||
|
if (!$showHidden && $name[0] === '.') continue;
|
||||||
|
|
||||||
|
$full = $realDir . '/' . $name;
|
||||||
|
$out[] = [
|
||||||
|
'name' => $name,
|
||||||
|
'virtual' => virtualize_path($full),
|
||||||
|
'isDir' => is_dir($full),
|
||||||
|
'ext' => pathinfo($name, PATHINFO_EXTENSION),
|
||||||
|
'size' => is_file($full) ? filesize($full) : null,
|
||||||
|
'mtime' => filemtime($full),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($out);
|
20
api/move.php
Normal file
20
api/move.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/../vfs.php';
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$srcV = $data['src'] ?? '';
|
||||||
|
$destV = $data['dest'] ?? '';
|
||||||
|
|
||||||
|
$srcReal = resolve_path($srcV);
|
||||||
|
$destReal = resolve_path($destV);
|
||||||
|
|
||||||
|
if (!file_exists($srcReal) || !is_dir($destReal)) {
|
||||||
|
echo json_encode(['success'=>false,'error'=>'Invalid paths']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$target = $destReal . '/' . basename($srcReal);
|
||||||
|
$ok = rename($srcReal, $target);
|
||||||
|
|
||||||
|
echo json_encode(['success'=>(bool)$ok]);
|
18
api/rename.php
Normal file
18
api/rename.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/../vfs.php';
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$oldV = $data['old'] ?? '';
|
||||||
|
$newName = $data['new'] ?? '';
|
||||||
|
|
||||||
|
$oldReal = resolve_path($oldV);
|
||||||
|
$newReal = dirname($oldReal) . '/' . basename($newName);
|
||||||
|
|
||||||
|
if (!file_exists($oldReal)) {
|
||||||
|
echo json_encode(['success'=>false,'error'=>'Not found']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ok = rename($oldReal, $newReal);
|
||||||
|
echo json_encode(['success'=>(bool)$ok]);
|
36
api/search.php
Normal file
36
api/search.php
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/../vfs.php';
|
||||||
|
|
||||||
|
$dir = $_GET['dir'] ?? '';
|
||||||
|
$q = $_GET['q'] ?? '';
|
||||||
|
$showHidden = ($_GET['hidden'] ?? '0') === '1';
|
||||||
|
|
||||||
|
$base = resolve_path($dir);
|
||||||
|
if (!is_dir($base)) {
|
||||||
|
echo json_encode([]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$out = [];
|
||||||
|
$it = new RecursiveIteratorIterator(
|
||||||
|
new RecursiveDirectoryIterator($base, FilesystemIterator::SKIP_DOTS)
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($it as $f) {
|
||||||
|
$name = $f->getFilename();
|
||||||
|
if (!$showHidden && strpos($name, '.') === 0) continue;
|
||||||
|
if (stripos($name, $q) === false) continue;
|
||||||
|
|
||||||
|
$full = $f->getPathname();
|
||||||
|
$out[] = [
|
||||||
|
'name' => $name,
|
||||||
|
'virtual' => virtualize_path($full),
|
||||||
|
'isDir' => $f->isDir(),
|
||||||
|
'ext' => $f->getExtension(),
|
||||||
|
'size' => $f->getSize(),
|
||||||
|
'mtime' => $f->getMTime(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($out);
|
63
delete.php
63
delete.php
@ -1,38 +1,41 @@
|
|||||||
<?php
|
<?php
|
||||||
$rootDir = realpath("/home/surillya/");
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Invalid request method']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
$data = json_decode(file_get_contents('php://input'), true);
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
if (empty($data['path'])) {
|
$path = $_SERVER['DOCUMENT_ROOT'] . '/' . $data['path'];
|
||||||
echo json_encode(['success' => false, 'error' => 'Missing path']);
|
|
||||||
|
if (!file_exists($path)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'File/folder does not exist']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve path properly
|
// Check if it's a directory
|
||||||
$delFullPath = realpath($rootDir . '/' . ltrim($data['path'], '/'));
|
if (is_dir($path)) {
|
||||||
|
// Delete recursively
|
||||||
// Validate path
|
deleteDirectory($path);
|
||||||
if (!$delFullPath || strpos($delFullPath, $rootDir) !== 0) {
|
|
||||||
echo json_encode(['success' => false, 'error' => 'Invalid path']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recursive deletion
|
|
||||||
function deleteRecursively($path) {
|
|
||||||
if (is_dir($path)) {
|
|
||||||
$items = scandir($path);
|
|
||||||
foreach ($items as $item) {
|
|
||||||
if ($item === '.' || $item === '..') continue;
|
|
||||||
deleteRecursively($path . DIRECTORY_SEPARATOR . $item);
|
|
||||||
}
|
|
||||||
return rmdir($path);
|
|
||||||
} elseif (is_file($path)) {
|
|
||||||
return unlink($path);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deleteRecursively($delFullPath)) {
|
|
||||||
echo json_encode(['success' => true]);
|
|
||||||
} else {
|
} else {
|
||||||
echo json_encode(['success' => false, 'error' => 'Delete failed']);
|
unlink($path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
function deleteDirectory($dir) {
|
||||||
|
if (!is_dir($dir)) return;
|
||||||
|
|
||||||
|
$files = scandir($dir);
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if ($file === '.' || $file === '..') continue;
|
||||||
|
$path = $dir . '/' . $file;
|
||||||
|
is_dir($path) ? deleteDirectory($path) : unlink($path);
|
||||||
|
}
|
||||||
|
rmdir($dir);
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
281
explorer.html
Normal file
281
explorer.html
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Surillya Explorer 2.0</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
background: rgba(30, 30, 30, 0.6);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="h-screen overflow-hidden text-gray-200">
|
||||||
|
|
||||||
|
<div id="tabs" class="flex items-center space-x-1 px-4 py-2 bg-transparent panel">
|
||||||
|
<button id="addTab" class="px-2 hover:bg-gray-700 rounded"><i class="fa fa-plus"></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="panels" class="flex h-[calc(100%-3rem)] overflow-hidden relative bg-transparent"></div>
|
||||||
|
|
||||||
|
<div id="ctxMenu" class="hidden absolute bg-gray-800 text-sm rounded shadow-lg z-50">
|
||||||
|
<ul>
|
||||||
|
<li data-action="info" class="px-4 py-2 hover:bg-gray-700 cursor-pointer">Show Info</li>
|
||||||
|
<li data-action="rename" class="px-4 py-2 hover:bg-gray-700 cursor-pointer">Rename</li>
|
||||||
|
<li data-action="delete" class="px-4 py-2 hover:bg-gray-700 cursor-pointer">Delete</li>
|
||||||
|
<li data-action="copyBuf" class="px-4 py-2 hover:bg-gray-700 cursor-pointer">Copy (buffer)</li>
|
||||||
|
<li data-action="cutBuf" class="px-4 py-2 hover:bg-gray-700 cursor-pointer">Cut (buffer)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// —— State ——
|
||||||
|
let tabs = [];
|
||||||
|
let activeTabId = null;
|
||||||
|
let clipboard = null;
|
||||||
|
|
||||||
|
window.fileAssoc = {};
|
||||||
|
fetch('/file_associations.json')
|
||||||
|
.then(r => r.json()).then(js => window.fileAssoc = js);
|
||||||
|
|
||||||
|
function basename(p) {
|
||||||
|
let f = p.split(/[\\/]/).pop().split(/[?#]/)[0];
|
||||||
|
return f.replace(/\.[^.]+$/, '');
|
||||||
|
}
|
||||||
|
function iconFor(ext, isDir) {
|
||||||
|
if (isDir) return '<i class="fa fa-folder"></i>';
|
||||||
|
let a = window.fileAssoc[ext] || {};
|
||||||
|
return `<i class="fa ${a.icon || 'fa-file'}"></i>`;
|
||||||
|
}
|
||||||
|
async function openFile(vp) {
|
||||||
|
let ext = vp.split('.').pop().toLowerCase(),
|
||||||
|
assoc = window.fileAssoc[ext];
|
||||||
|
if (!assoc) {
|
||||||
|
parent.notify("Explorer", `No app for .${ext}`, { type: 'error', icon: '⚠️', timeout: 3000 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
parent.openApp(`${assoc.app}?q=${encodeURIComponent(vp)}`, basename(vp));
|
||||||
|
}
|
||||||
|
|
||||||
|
function newTab(initial = '') {
|
||||||
|
let id = Date.now().toString();
|
||||||
|
|
||||||
|
let btn = document.createElement('button');
|
||||||
|
btn.className = 'tab flex items-center px-3 hover:bg-gray-700 rounded';
|
||||||
|
btn.innerHTML = `<span>${initial || '~'}</span>
|
||||||
|
<span class="closeTab ml-2 text-xs">×</span>`;
|
||||||
|
btn.querySelector('span').onclick = () => activateTab(id);
|
||||||
|
btn.querySelector('.closeTab').onclick = e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
closeTab(id);
|
||||||
|
};
|
||||||
|
document.getElementById('tabs')
|
||||||
|
.insertBefore(btn, document.getElementById('addTab'));
|
||||||
|
|
||||||
|
let panel = document.createElement('div');
|
||||||
|
panel.className = 'panel flex-1 m-2 p-4 flex flex-col overflow-hidden hidden bg-transparent';
|
||||||
|
panel.dataset.id = id;
|
||||||
|
panel.innerHTML = `
|
||||||
|
<div class="flex justify-between mb-2">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<button class="navBtn" data-dir="..">←</button>
|
||||||
|
<button class="toggleHidden">.files</button>
|
||||||
|
<button class="pasteBtn px-2 bg-gray-700 text-sm rounded opacity-50 cursor-not-allowed" disabled>
|
||||||
|
Paste
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input class="searchBox px-2 bg-transparent rounded" placeholder="Search…"/>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-4 gap-4 flex-1 overflow-auto fileGrid bg-transparent"></div>`;
|
||||||
|
document.getElementById('panels').append(panel);
|
||||||
|
|
||||||
|
tabs.push({ id, cwd: initial, hidden: false, tabBtn: btn, panelEl: panel });
|
||||||
|
activateTab(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateTab(id) {
|
||||||
|
activeTabId = id;
|
||||||
|
tabs.forEach(t => {
|
||||||
|
t.tabBtn.classList.toggle('bg-gray-600', t.id === id);
|
||||||
|
t.panelEl.classList.toggle('hidden', t.id !== id);
|
||||||
|
});
|
||||||
|
loadDir(id);
|
||||||
|
}
|
||||||
|
function closeTab(id) {
|
||||||
|
let i = tabs.findIndex(t => t.id === id);
|
||||||
|
if (i < 0) return;
|
||||||
|
tabs[i].tabBtn.remove();
|
||||||
|
tabs[i].panelEl.remove();
|
||||||
|
tabs.splice(i, 1);
|
||||||
|
if (activeTabId === id) {
|
||||||
|
if (tabs[i]) activateTab(tabs[i].id);
|
||||||
|
else if (tabs[i - 1]) activateTab(tabs[i - 1].id);
|
||||||
|
else newTab('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.getElementById('addTab').onclick = () => newTab('');
|
||||||
|
|
||||||
|
function loadDir(id, query = '') {
|
||||||
|
let tab = tabs.find(t => t.id === id),
|
||||||
|
dir = tab.cwd,
|
||||||
|
hidden = tab.hidden ? 1 : 0,
|
||||||
|
url = query
|
||||||
|
? `/api/search.php?dir=${encodeURIComponent(dir)}&hidden=${hidden}&q=${encodeURIComponent(query)}`
|
||||||
|
: `/api/list.php?dir=${encodeURIComponent(dir)}&hidden=${hidden}`;
|
||||||
|
|
||||||
|
fetch(url).then(r => r.json()).then(items => {
|
||||||
|
tab.tabBtn.firstChild.textContent = dir ? basename(dir) : '~';
|
||||||
|
|
||||||
|
let grid = tab.panelEl.querySelector('.fileGrid');
|
||||||
|
grid.innerHTML = '';
|
||||||
|
items.forEach(it => {
|
||||||
|
let card = document.createElement('div');
|
||||||
|
card.className = 'p-3 bg-gray-700 rounded cursor-pointer flex items-center space-x-2';
|
||||||
|
card.draggable = true;
|
||||||
|
card.dataset.virtual = it.virtual;
|
||||||
|
card.dataset.isdir = it.isDir;
|
||||||
|
card.innerHTML = `${iconFor(it.ext, it.isDir)}<span class="truncate">${it.name}</span>`;
|
||||||
|
|
||||||
|
card.ondblclick = () => {
|
||||||
|
if (it.isDir) {
|
||||||
|
tab.cwd = it.virtual;
|
||||||
|
loadDir(id);
|
||||||
|
} else openFile(it.virtual);
|
||||||
|
};
|
||||||
|
card.addEventListener('dragstart', e => {
|
||||||
|
clipboard = null; // clear buffer when dragging
|
||||||
|
e.dataTransfer.setData('text/plain', it.virtual);
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
});
|
||||||
|
card.addEventListener('contextmenu', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
showContextMenu(e.pageX, e.pageY, card);
|
||||||
|
});
|
||||||
|
grid.append(card);
|
||||||
|
});
|
||||||
|
|
||||||
|
let h = tab.panelEl;
|
||||||
|
h.querySelector('.navBtn').onclick = () => {
|
||||||
|
let parts = tab.cwd.split('/').filter(Boolean);
|
||||||
|
parts.pop();
|
||||||
|
tab.cwd = parts.join('/');
|
||||||
|
loadDir(id);
|
||||||
|
};
|
||||||
|
h.querySelector('.toggleHidden').onclick = () => {
|
||||||
|
tab.hidden = !tab.hidden;
|
||||||
|
loadDir(id);
|
||||||
|
};
|
||||||
|
let sb = h.querySelector('.searchBox');
|
||||||
|
sb.oninput = e => loadDir(id, e.target.value);
|
||||||
|
|
||||||
|
let pasteBtn = h.querySelector('.pasteBtn');
|
||||||
|
if (clipboard) {
|
||||||
|
pasteBtn.disabled = false;
|
||||||
|
pasteBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||||
|
pasteBtn.textContent = `Paste (${clipboard.op})`;
|
||||||
|
} else {
|
||||||
|
pasteBtn.disabled = true;
|
||||||
|
pasteBtn.classList.add('opacity-50', 'cursor-not-allowed');
|
||||||
|
pasteBtn.textContent = 'Paste';
|
||||||
|
}
|
||||||
|
pasteBtn.onclick = () => {
|
||||||
|
if (!clipboard) return;
|
||||||
|
apiAction(clipboard.op, clipboard.virtual, tab.cwd, () => {
|
||||||
|
clipboard = null;
|
||||||
|
loadDir(id);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentCard = null;
|
||||||
|
const ctx = document.getElementById('ctxMenu');
|
||||||
|
|
||||||
|
function showContextMenu(x, y, card) {
|
||||||
|
currentCard = card;
|
||||||
|
ctx.style.left = x + 'px';
|
||||||
|
ctx.style.top = y + 'px';
|
||||||
|
ctx.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
document.addEventListener('click', () => ctx.classList.add('hidden'));
|
||||||
|
|
||||||
|
ctx.querySelectorAll('li').forEach(li => {
|
||||||
|
li.onclick = () => {
|
||||||
|
let act = li.dataset.action,
|
||||||
|
v = currentCard.dataset.virtual,
|
||||||
|
isD = currentCard.dataset.isdir === '1';
|
||||||
|
|
||||||
|
switch (act) {
|
||||||
|
case 'info':
|
||||||
|
fetch(`/api/info.php?path=${encodeURIComponent(v)}`)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(i => alert(`Name: ${i.name}\nType: ${i.type}\nSize: ${i.size}\nModified: ${i.mtime}`));
|
||||||
|
break;
|
||||||
|
case 'rename':
|
||||||
|
let nn = prompt('Rename to', basename(v));
|
||||||
|
if (nn && nn !== basename(v)) {
|
||||||
|
fetch('/api/rename.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ old: v, new: nn })
|
||||||
|
}).then(r => r.json()).then(j => {
|
||||||
|
if (!j.success) alert('Rename failed: ' + j.error);
|
||||||
|
else loadDir(activeTabId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
if (confirm(`Delete "${basename(v)}"?`)) {
|
||||||
|
fetch('/api/delete.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ path: v })
|
||||||
|
}).then(r => r.json()).then(j => {
|
||||||
|
if (!j.success) alert('Delete error: ' + j.error);
|
||||||
|
else loadDir(activeTabId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'copyBuf':
|
||||||
|
case 'cutBuf':
|
||||||
|
clipboard = {
|
||||||
|
virtual: v,
|
||||||
|
isDir: isD,
|
||||||
|
op: act === 'copyBuf' ? 'copy' : 'move'
|
||||||
|
};
|
||||||
|
parent.notify("Explorer", `${act === 'copyBuf' ? 'Copied' : 'Cut'} to buffer`, {
|
||||||
|
type: 'info', icon: '📋', timeout: 2000
|
||||||
|
});
|
||||||
|
loadDir(activeTabId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ctx.classList.add('hidden');
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function apiAction(action, src, dest, cb) {
|
||||||
|
fetch(`/api/${action}.php`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ src, dest })
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(j => {
|
||||||
|
if (!j.success) alert(`${action} error: ${j.error}`);
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
newTab('');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
298
explorer.php
298
explorer.php
@ -1,298 +0,0 @@
|
|||||||
<?php
|
|
||||||
function getAssociatedApp($extension)
|
|
||||||
{
|
|
||||||
static $associations = null;
|
|
||||||
if ($associations === null) {
|
|
||||||
$json = file_get_contents('file_associations.json');
|
|
||||||
$associations = json_decode($json, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $associations[strtolower($extension)] ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$root = realpath("/home/surillya");
|
|
||||||
$cdir = isset($_GET['dir']) ? $_GET['dir'] : '';
|
|
||||||
$dir = realpath($root . DIRECTORY_SEPARATOR . $cdir);
|
|
||||||
|
|
||||||
if (!$dir || strpos($dir, $root) !== 0 || !is_dir($dir)) {
|
|
||||||
echo "Invalid directory.";
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$items = scandir($dir);
|
|
||||||
$relPath = str_replace($root, '', $dir);
|
|
||||||
|
|
||||||
function humanFileSize($size)
|
|
||||||
{
|
|
||||||
if ($size < 1024)
|
|
||||||
return $size . ' B';
|
|
||||||
if ($size < 1048576)
|
|
||||||
return round($size / 1024, 2) . ' KB';
|
|
||||||
if ($size < 1073741824)
|
|
||||||
return round($size / 1048576, 2) . ' MB';
|
|
||||||
return round($size / 1073741824, 2) . ' GB';
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Surillya Explorer</title>
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--accent: #61dafb;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: sans-serif;
|
|
||||||
background: transparent;
|
|
||||||
color: #eee;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--accent);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, p {
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.explorer {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
|
||||||
gap: 10px;
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 8px;
|
|
||||||
gap: 10px;
|
|
||||||
background: #222;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-item:hover {
|
|
||||||
background: #2a2a2a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-icon {
|
|
||||||
font-size: 32px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.context-menu {
|
|
||||||
position: absolute;
|
|
||||||
background: #222;
|
|
||||||
border: 1px solid #555;
|
|
||||||
border-radius: 8px;
|
|
||||||
display: none;
|
|
||||||
z-index: 999;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.context-menu .op {
|
|
||||||
padding: 8px 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.context-menu .op:hover {
|
|
||||||
background: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filename {
|
|
||||||
display: block;
|
|
||||||
max-width: 100%;
|
|
||||||
flex-grow: 1;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<h1>THOS Explorer</h1>
|
|
||||||
<p>Current directory: <code><?php echo htmlspecialchars($cdir ?: '/'); ?></code></p>
|
|
||||||
|
|
||||||
<?php
|
|
||||||
if ($dir !== $root) {
|
|
||||||
$parentDir = dirname($cdir);
|
|
||||||
echo '<p><a href="?dir=' . urlencode($parentDir) . '">⬅️ Go Up</a></p>';
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<div class="explorer" id="fileGrid">
|
|
||||||
<?php
|
|
||||||
foreach ($items as $item) {
|
|
||||||
if ($item === '.' || $item === '..')
|
|
||||||
continue;
|
|
||||||
$itemPath = $dir . DIRECTORY_SEPARATOR . $item;
|
|
||||||
$relItem = ltrim($relPath . DIRECTORY_SEPARATOR . $item, DIRECTORY_SEPARATOR);
|
|
||||||
$isDir = is_dir($itemPath);
|
|
||||||
|
|
||||||
$ext = pathinfo($item, PATHINFO_EXTENSION);
|
|
||||||
$app = getAssociatedApp($ext);
|
|
||||||
$icon = $isDir ? '📁' : ($app['icon'] ?? '📄');
|
|
||||||
|
|
||||||
if ($isDir)
|
|
||||||
echo '<a href="?dir=' . urlencode($relItem) . '">';
|
|
||||||
else {
|
|
||||||
require_once "vfs.php";
|
|
||||||
$vPath = virtualize_path($itemPath);
|
|
||||||
echo '<a href="javascript:void(0);" onclick="openFile(\'' . htmlspecialchars($vPath) . '\')">';
|
|
||||||
}
|
|
||||||
echo '<div class="item" data-name="' . htmlspecialchars($item) . '" data-path="' . htmlspecialchars($relItem) . '" data-isdir="' . ($isDir ? '1' : '0') . '">';
|
|
||||||
echo '<div class="file-icon">' . $icon . '</div>';
|
|
||||||
echo '<span class="filename">' . htmlspecialchars($item) . '</span>';
|
|
||||||
echo '</div></a>';
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="context-menu" id="contextMenu">
|
|
||||||
<div class="op" id="info">Show Info</div>
|
|
||||||
<div class="op" id="rename">Rename</div>
|
|
||||||
<div class="op" id="delete">Delete</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function basename(inputPath) {
|
|
||||||
if (!inputPath) return '';
|
|
||||||
|
|
||||||
const cleanPath = inputPath.split(/[?#]/)[0];
|
|
||||||
|
|
||||||
const segments = cleanPath.split('/');
|
|
||||||
const fileName = segments.pop() || '';
|
|
||||||
|
|
||||||
return fileName.replace(/\.[^.]+$/, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async function openFile(virtualPath) {
|
|
||||||
const ext = virtualPath.split('.').pop().toLowerCase();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch('file_associations.json');
|
|
||||||
const associations = await res.json();
|
|
||||||
|
|
||||||
if (associations[ext]) {
|
|
||||||
const appUrl = associations[ext].app + "?q=" + encodeURIComponent(virtualPath);
|
|
||||||
parent.openApp(appUrl, basename(virtualPath));
|
|
||||||
} else {
|
|
||||||
window.parent.notify("Explorer", `No app found for ".${ext}" files`, {
|
|
||||||
type: "error",
|
|
||||||
icon: "⚠️",
|
|
||||||
timeout: 3000
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to fetch file associations:', e);
|
|
||||||
window.parent.notify("Explorer", "Failed to open file", {
|
|
||||||
type: "error",
|
|
||||||
icon: "⚠️",
|
|
||||||
timeout: 3000
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// function showToast(message) {
|
|
||||||
// const toast = document.createElement('div');
|
|
||||||
// toast.innerText = message;
|
|
||||||
// toast.style.position = 'fixed';
|
|
||||||
// toast.style.bottom = '20px';
|
|
||||||
// toast.style.left = '20px';
|
|
||||||
// toast.style.background = '#333';
|
|
||||||
// toast.style.color = '#fff';
|
|
||||||
// toast.style.padding = '10px 20px';
|
|
||||||
// toast.style.borderRadius = '8px';
|
|
||||||
// toast.style.zIndex = '9999';
|
|
||||||
// document.body.appendChild(toast);
|
|
||||||
// setTimeout(() => toast.remove(), 3000);
|
|
||||||
// }
|
|
||||||
|
|
||||||
let contextMenu = document.getElementById("contextMenu");
|
|
||||||
let currentTarget = null;
|
|
||||||
document.getElementById('fileGrid').addEventListener('contextmenu', e => {
|
|
||||||
e.preventDefault();
|
|
||||||
const item = e.target.closest('.item');
|
|
||||||
if (!item) {
|
|
||||||
contextMenu.style.display = 'none';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
currentTarget = item;
|
|
||||||
|
|
||||||
contextMenu.style.top = `${e.clientY}px`;
|
|
||||||
contextMenu.style.left = `${e.clientX}px`;
|
|
||||||
contextMenu.style.display = 'block';
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener("click", function () {
|
|
||||||
contextMenu.style.display = "none";
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('info').addEventListener('click', () => {
|
|
||||||
contextMenu.style.display = 'none';
|
|
||||||
if (!currentTarget) return;
|
|
||||||
|
|
||||||
fetch('file_info.php?path=' + encodeURIComponent(currentTarget.dataset.path))
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(info => {
|
|
||||||
alert(`Name: ${info.name}\nType: ${info.type}\nSize: ${info.size}\nModified: ${info.mtime}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('rename').addEventListener('click', () => {
|
|
||||||
contextMenu.style.display = 'none';
|
|
||||||
if (!currentTarget) return;
|
|
||||||
|
|
||||||
const oldName = currentTarget.dataset.name;
|
|
||||||
const newName = prompt("Rename file/folder", oldName);
|
|
||||||
if (!newName || newName === oldName) return;
|
|
||||||
|
|
||||||
fetch('rename.php', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ oldName: currentTarget.dataset.path, newName })
|
|
||||||
}).then(res => res.json())
|
|
||||||
.then(resp => {
|
|
||||||
if (resp.success) {
|
|
||||||
showToast('Renamed successfully!');
|
|
||||||
location.reload();
|
|
||||||
} else {
|
|
||||||
showToast('Rename failed: ' + resp.error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('delete').addEventListener('click', () => {
|
|
||||||
contextMenu.style.display = 'none';
|
|
||||||
if (!currentTarget) return;
|
|
||||||
|
|
||||||
if (!confirm(`Are you sure you want to delete "${currentTarget.dataset.name}"?`)) return;
|
|
||||||
|
|
||||||
fetch('delete.php', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ path: currentTarget.dataset.path })
|
|
||||||
}).then(res => res.json())
|
|
||||||
.then(resp => {
|
|
||||||
if (resp.success) {
|
|
||||||
showToast('Deleted successfully!');
|
|
||||||
location.reload();
|
|
||||||
} else {
|
|
||||||
showToast('Delete failed: ' + resp.error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.documentElement.style.setProperty('--accent', window.parent.THOS.getAllSettings().accentColor || '#ff69b4');
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"jpg": {"app":"imager.php", "icon":"🖼️"},
|
"jpg": {"app":"imager.php", "icon":"fa-file-image"},
|
||||||
"jpeg": {"app":"imager.php", "icon":"🖼️"},
|
"jpeg": {"app":"imager.php", "icon":"fa-file-image"},
|
||||||
"png": {"app":"imager.php", "icon":"🖼️"},
|
"png": {"app":"imager.php", "icon":"fa-file-image"},
|
||||||
"gif": {"app":"imager.php", "icon":"🖼️"},
|
"gif": {"app":"imager.php", "icon":"fa-file-image"},
|
||||||
"webp": {"app":"imager.php", "icon":"🖼️"},
|
"webp": {"app":"imager.php", "icon":"fa-file-image"},
|
||||||
"mp3": {"app":"player.php", "icon":"🎵"},
|
"mp3": {"app":"player.php", "icon":"fa-file-music"},
|
||||||
"wav": {"app":"player.php", "icon":"🎵"},
|
"wav": {"app":"player.php", "icon":"fa-file-music"},
|
||||||
"opus": {"app":"player.php", "icon":"🎵"},
|
"opus": {"app":"player.php", "icon":"fa-file-music"},
|
||||||
"m4a": {"app":"player.php", "icon":"🎵"},
|
"m4a": {"app":"player.php", "icon":"fa-file-music"},
|
||||||
"thp": {"app":"thp.php", "icon":"📦"},
|
"thp": {"app":"thp.php", "icon":"fa-file-archive"},
|
||||||
"mp4": {"app":"video.php", "icon":"📽️"},
|
"mp4": {"app":"video.php", "icon":"fa-file-video"},
|
||||||
"webm": {"app":"video.php", "icon":"📽️"}
|
"webm": {"app":"video.php", "icon":"fa-file-video"}
|
||||||
}
|
}
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
$rootDir = realpath("/home/surillya/"); // This should be absolute
|
|
||||||
$path = $_GET['path'] ?? '';
|
|
||||||
$fullPath = $rootDir . '/' . ltrim($path, '/'); // Fix slash issues
|
|
||||||
$realFullPath = realpath($fullPath);
|
|
||||||
|
|
||||||
// Check if the resolved path is still within rootDir (for security)
|
|
||||||
if (!$realFullPath || strpos($realFullPath, $rootDir) !== 0) {
|
|
||||||
http_response_code(404);
|
|
||||||
echo json_encode(['error' => 'File not found or access denied']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now do the info check
|
|
||||||
$info = [];
|
|
||||||
$info['name'] = basename($realFullPath);
|
|
||||||
$info['type'] = is_dir($realFullPath) ? 'Directory' : mime_content_type($realFullPath);
|
|
||||||
$info['size'] = is_file($realFullPath) ? filesize($realFullPath) : 0;
|
|
||||||
$info['mtime'] = date("Y-m-d H:i:s", filemtime($realFullPath));
|
|
||||||
|
|
||||||
if (is_file($realFullPath)) {
|
|
||||||
if ($info['size'] < 1024) $info['size'] = $info['size'] . ' B';
|
|
||||||
else if ($info['size'] < 1048576) $info['size'] = round($info['size'] / 1024, 2) . ' KB';
|
|
||||||
else if ($info['size'] < 1073741824) $info['size'] = round($info['size'] / 1048576, 2) . ' MB';
|
|
||||||
else $info['size'] = round($info['size'] / 1073741824, 2) . ' GB';
|
|
||||||
}
|
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
|
||||||
echo json_encode($info);
|
|
@ -307,7 +307,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-3 gap-3 p-2 flex flex-col justify-between">
|
<div class="grid grid-cols-3 gap-3 p-2 flex flex-col justify-between">
|
||||||
<button onclick="openApp('explorer.php', 'Explorer')" title="File Explorer"
|
<button onclick="openApp('explorer.html', 'Explorer')" title="File Explorer"
|
||||||
class="p-2 rounded-lg bg-gray-800 hover:bg-gray-700 transition-all">
|
class="p-2 rounded-lg bg-gray-800 hover:bg-gray-700 transition-all">
|
||||||
<svg class="w-6 h-6 text-blue-300 mx-auto" fill="none" stroke="currentColor" stroke-width="2"
|
<svg class="w-6 h-6 text-blue-300 mx-auto" fill="none" stroke="currentColor" stroke-width="2"
|
||||||
viewBox="0 0 24 24">
|
viewBox="0 0 24 24">
|
||||||
|
31
rename.php
31
rename.php
@ -1,31 +0,0 @@
|
|||||||
<?php
|
|
||||||
$rootDir = realpath("/home/surillya/");
|
|
||||||
$data = json_decode(file_get_contents('php://input'), true);
|
|
||||||
|
|
||||||
if (empty($data['oldName']) || empty($data['newName'])) {
|
|
||||||
echo json_encode(['success' => false, 'error' => 'Missing parameters']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve paths properly
|
|
||||||
$oldFullPath = realpath($rootDir . '/' . ltrim($data['oldName'], '/'));
|
|
||||||
$newFullPath = $rootDir . '/' . ltrim(dirname($data['oldName']), '/') . '/' . basename($data['newName']);
|
|
||||||
$newFullPath = realpath(dirname($newFullPath)) . '/' . basename($data['newName']); // Ensure dir exists
|
|
||||||
|
|
||||||
// Validate paths
|
|
||||||
if (!$oldFullPath || strpos($oldFullPath, $rootDir) !== 0) {
|
|
||||||
echo json_encode(['success' => false, 'error' => 'Invalid old path']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file_exists($newFullPath)) {
|
|
||||||
echo json_encode(['success' => false, 'error' => 'New name already exists']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt rename
|
|
||||||
if (rename($oldFullPath, $newFullPath)) {
|
|
||||||
echo json_encode(['success' => true]);
|
|
||||||
} else {
|
|
||||||
echo json_encode(['success' => false, 'error' => 'Rename failed']);
|
|
||||||
}
|
|
20
vfs.php
20
vfs.php
@ -1,16 +1,26 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once "config.php";
|
// vfs.php
|
||||||
|
// Must sit in your project root (or adjust the require path below)
|
||||||
|
require_once __DIR__ . '/config.php';
|
||||||
|
|
||||||
// Converts virtual path like "/Documents/file.txt" → real path like "/home/surillya/Documents/file.txt"
|
/**
|
||||||
|
* Converts a virtual path ("/Documents/file.txt")
|
||||||
|
* into a real filesystem path under REAL_ROOT.
|
||||||
|
*/
|
||||||
function resolve_path(string $virtualPath): string {
|
function resolve_path(string $virtualPath): string {
|
||||||
$virtualPath = str_replace('..', '', $virtualPath); // prevent traversal
|
// strip any “..” just in case
|
||||||
|
$virtualPath = str_replace('..', '', $virtualPath);
|
||||||
return rtrim(REAL_ROOT, '/') . '/' . ltrim($virtualPath, '/');
|
return rtrim(REAL_ROOT, '/') . '/' . ltrim($virtualPath, '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Converts real path like "/home/surillya/Documents/file.txt" → virtual path like "/Documents/file.txt"
|
/**
|
||||||
|
* Converts a real filesystem path back into a virtual one
|
||||||
|
* (so the front-end never sees your real server layout).
|
||||||
|
*/
|
||||||
function virtualize_path(string $realPath): string {
|
function virtualize_path(string $realPath): string {
|
||||||
if (str_starts_with($realPath, REAL_ROOT)) {
|
if (str_starts_with($realPath, REAL_ROOT)) {
|
||||||
return '/' . ltrim(substr($realPath, strlen(REAL_ROOT)), '/');
|
return '/' . ltrim(substr($realPath, strlen(REAL_ROOT)), '/');
|
||||||
}
|
}
|
||||||
return $realPath; // fallback, possibly invalid or external
|
// fallback (shouldn’t happen if you always resolve under REAL_ROOT)
|
||||||
|
return $realPath;
|
||||||
}
|
}
|
Reference in New Issue
Block a user