fix: Window Explorer liệt kê ổ đĩa thật + xử lý lỗi API

This commit is contained in:
2026-08-02 14:27:03 +07:00
parent d1995307f5
commit 1989bfb105
5 changed files with 79 additions and 27 deletions
+43 -10
View File
@@ -24,42 +24,75 @@ def _safe_path(path: str) -> str:
return os.path.normpath(path) return os.path.normpath(path)
REAL_FS_TYPES = {
"ext2", "ext3", "ext4", "xfs", "btrfs", "jfs", "reiserfs",
"ntfs", "ntfs3", "vfat", "exfat", "fat", "hfs", "hfsplus", "apfs",
"zfs", "f2fs", "iso9660", "udf", "nfs", "nfs4", "cifs", "smb3", "fuseblk",
}
PSEUDO_FS_TYPES = {
"proc", "sysfs", "devpts", "tmpfs", "devtmpfs", "overlay", "squashfs",
"cgroup", "cgroup2", "pstore", "securityfs", "debugfs", "tracefs",
"configfs", "fusectl", "hugetlbfs", "mqueue", "binfmt_misc", "nsfs",
"autofs", "ramfs", "efivarfs", "rpc_pipefs", "fuse", "fusefs",
}
@router.get("/computer") @router.get("/computer")
async def list_computer_roots(): async def list_computer_roots():
"""Liệt kê các ổ đĩa / mount point của máy (My Computer).""" """Liệt kê các ổ đĩa / mount point thật của máy (My Computer)."""
system = platform.system() system = platform.system()
roots = [] roots = []
if system == "Windows": if system == "Windows":
import string import string
from ctypes import windll
for drive in string.ascii_uppercase: for drive in string.ascii_uppercase:
root = drive + ":\\" root = drive + ":\\"
if os.path.exists(root): try:
roots.append({"path": root, "name": drive + ":", "is_dir": True}) if os.path.exists(root):
roots.append({"path": root, "name": drive + ":", "is_dir": True})
except OSError:
continue
else: else:
# Unix/Linux/macOS: liệt kê mount points từ /proc/mounts # Unix/Linux/macOS: chỉ liệt kê filesystem thật, bỏ pseudo/docker/systemd mounts
seen = set() seen = set()
try: try:
with open("/proc/mounts", "r") as f: with open("/proc/mounts", "r") as f:
for line in f: for line in f:
parts = line.split() parts = line.split()
if len(parts) < 2: if len(parts) < 3:
continue continue
mount = parts[1] device, mount, fstype = parts[0], parts[1], parts[2]
if fstype in PSEUDO_FS_TYPES:
continue
if fstype not in REAL_FS_TYPES:
# giữ mount point root "/" nếu không thuộc pseudo
if mount != "/":
continue
if mount in seen: if mount in seen:
continue continue
seen.add(mount) seen.add(mount)
if mount.startswith("/dev") or mount.startswith("/sys") or mount.startswith("/proc"): # lọc mount point rác kiểu /run/credentials/...
if mount.startswith("/run/") or mount.startswith("/var/lib/docker"):
continue continue
try: try:
if os.path.isdir(mount): if os.path.isdir(mount):
roots.append({"path": mount, "name": mount, "is_dir": True}) label = mount if mount != "/" else "Root (/)"
roots.append({"path": mount, "name": label, "is_dir": True})
except OSError: except OSError:
pass pass
except OSError: except OSError:
pass pass
# macOS fallback: liệt kê /Volumes
if system == "Darwin":
try:
for name in sorted(os.listdir("/Volumes")):
full = os.path.join("/Volumes", name)
if os.path.isdir(full):
roots.append({"path": full, "name": name, "is_dir": True})
except OSError:
pass
if not roots: if not roots:
roots = [{"path": "/", "name": "/", "is_dir": True}] roots = [{"path": "/", "name": "Root (/)", "is_dir": True}]
return {"system": system, "roots": roots} return {"system": system, "roots": roots}
+27 -14
View File
@@ -9045,7 +9045,8 @@ const MediaExplorerPanel = ({ height }) => {
const fileDuration = f => { const fileDuration = f => {
if (!f) return 0; if (!f) return 0;
if (isMidiFile(f)) return (f.lengthQn || 16) * 60 / (f.bpm || 120); if (isMidiFile(f)) return (f.lengthQn || 16) * 60 / (f.bpm || 120);
return f.duration || (audioBuffer && selected && (selected.file_id || selected.fileId) === (f.file_id || f.fileId) ? audioBuffer.duration : 0) || 0; const matches = selected && ((f.path && f.path === selected.path) || (!f.path && (f.file_id || f.fileId) === (selected.file_id || selected.fileId)));
return f.duration || (audioBuffer && matches ? audioBuffer.duration : 0) || 0;
}; };
const folderFiles = React.useMemo(() => { const folderFiles = React.useMemo(() => {
@@ -9097,18 +9098,26 @@ const MediaExplorerPanel = ({ height }) => {
const openMyComputer = async () => { const openMyComputer = async () => {
setFolder('computer'); setFolder('computer');
if (!computerRoots) { if (computerRoots) return;
try { setComputerRoots(null);
const resp = await fetch(`${API_BASE_URL}/api/v1/media/computer`); try {
const data = await resp.json(); const resp = await fetch(`${API_BASE_URL}/api/v1/media/computer`);
setComputerRoots(data.roots || []); if (!resp.ok) throw new Error('HTTP ' + resp.status);
} catch (e) { setComputerRoots([]); } const data = await resp.json();
const roots = data.roots || [];
setComputerRoots(roots.length ? roots : [{ path: '/', name: 'Root (/)', is_dir: true }]);
if (!roots.length) window.showToast && window.showToast('Không tìm thấy ổ đĩa nào', 'warning');
} catch (e) {
setComputerRoots([{ path: '/', name: 'Root (/)', is_dir: true }]);
window.showToast && window.showToast('Không thể truy cập My Computer: ' + e.message, 'error');
} }
}; };
const browseComputerDir = async (path) => { const browseComputerDir = async (path) => {
if (!path) return;
try { try {
const resp = await fetch(`${API_BASE_URL}/api/v1/media/browse?path=${encodeURIComponent(path)}`); const resp = await fetch(`${API_BASE_URL}/api/v1/media/browse?path=${encodeURIComponent(path)}`);
if (!resp.ok) throw new Error('HTTP ' + resp.status);
const data = await resp.json(); const data = await resp.json();
setComputerPath(data.path); setComputerPath(data.path);
setComputerFiles(data.files || []); setComputerFiles(data.files || []);
@@ -9117,8 +9126,7 @@ const MediaExplorerPanel = ({ height }) => {
stopMediaPlayback(); stopMediaPlayback();
setComputerTree(prev => ({ ...prev, [path]: { dirs: data.dirs || [], expanded: true } })); setComputerTree(prev => ({ ...prev, [path]: { dirs: data.dirs || [], expanded: true } }));
} catch (e) { } catch (e) {
setComputerPath(path); window.showToast && window.showToast('Không thể mở thư mục: ' + e.message, 'error');
setComputerFiles([]);
} }
}; };
@@ -9133,10 +9141,15 @@ const MediaExplorerPanel = ({ height }) => {
const goComputerParent = () => { const goComputerParent = () => {
if (!computerPath) return; if (!computerPath) return;
const parent = computerPath.split(/[\\/]/).filter(Boolean); const isUnix = computerPath.startsWith('/');
parent.pop(); const parts = computerPath.split(/[\\/]/).filter(Boolean);
const parentPath = (parent.length === 0 ? (computerPath.startsWith('/') ? '/' : '') : (computerPath.startsWith('/') ? '/' + parent.join('/') : parent.join('\\'))); parts.pop();
browseComputerDir(parentPath); if (isUnix) {
browseComputerDir(parts.length ? '/' + parts.join('/') : '/');
} else {
// Windows: quay v root đĩa nếu đã lên ti đnh
browseComputerDir(parts.length ? parts.join('\\') : computerPath.split(/[\\/]/)[0] + '\\');
}
}; };
const filePreviewUrl = (f) => { const filePreviewUrl = (f) => {
@@ -9416,7 +9429,7 @@ const MediaExplorerPanel = ({ height }) => {
return ( return (
<tr key={(f.file_id || f.name) + i} className={`cursor-pointer hover:bg-blue-100 ${isSel ? 'file-row-selected' : ''}`} onClick={() => handleSelect(f)}> <tr key={(f.file_id || f.name) + i} className={`cursor-pointer hover:bg-blue-100 ${isSel ? 'file-row-selected' : ''}`} onClick={() => handleSelect(f)}>
<td className="py-1 px-2"><i className={`fa-solid ${isMidi ? 'fa-music text-purple-600' : 'fa-file-audio text-emerald-600'} mr-2`}></i>{f.name || f.original_name}</td> <td className="py-1 px-2"><i className={`fa-solid ${isMidi ? 'fa-music text-purple-600' : 'fa-file-audio text-emerald-600'} mr-2`}></i>{f.name || f.original_name}</td>
{viewMode === 'details' && <td className="py-1 px-2">{f.size_mb != null ? f.size_mb.toFixed(2) + ' MB' : (isMidi ? f.tpqn + ' TPQN' : '-')}</td>} {viewMode === 'details' && <td className="py-1 px-2">{f.size_mb != null ? f.size_mb.toFixed(2) + ' MB' : (isMidi ? (f.tpqn || 'MIDI') + ' TPQN' : '-')}</td>}
</tr> </tr>
); );
})} })}
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -23,7 +23,7 @@
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script> <script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script> <script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script> <script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
<script src="/static/js/app.precompiled.js?v=202607311305" defer></script> <script src="/static/js/app.precompiled.js?v=202608021415" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016"> <link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style> <style>
:root { :root {
+5
View File
@@ -1074,3 +1074,8 @@
- **Tóm tắt thay đổi:** Backend mới `app/api/v1/media.py` (+ mount trong `app/main.py`): `GET /api/v1/media/computer` liệt kê ổ đĩa/mount point (Windows drive letters / Linux `/proc/mounts`), `GET /api/v1/media/browse?path=` liệt kê thư mục con + file audio/MIDI, `GET /api/v1/media/file?path=` serve file cục bộ để preview (chặn ext không phải media). Frontend `MediaExplorerPanel`: node "My Computer" click → fetch roots, expand/collapse tree (2 cấp con), click thư mục → liệt kê file vào list, preview audio local qua `/media/file`, nút Back/Up/Refresh + address bar hiểu `folder === 'computer'`; metadata hiển thị Local Audio/Local MIDI. - **Tóm tắt thay đổi:** Backend mới `app/api/v1/media.py` (+ mount trong `app/main.py`): `GET /api/v1/media/computer` liệt kê ổ đĩa/mount point (Windows drive letters / Linux `/proc/mounts`), `GET /api/v1/media/browse?path=` liệt kê thư mục con + file audio/MIDI, `GET /api/v1/media/file?path=` serve file cục bộ để preview (chặn ext không phải media). Frontend `MediaExplorerPanel`: node "My Computer" click → fetch roots, expand/collapse tree (2 cấp con), click thư mục → liệt kê file vào list, preview audio local qua `/media/file`, nút Back/Up/Refresh + address bar hiểu `folder === 'computer'`; metadata hiển thị Local Audio/Local MIDI.
- **Các file ảnh hưởng:** `app/api/v1/media.py`, `app/main.py`, `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/templates/index.html` - **Các file ảnh hưởng:** `app/api/v1/media.py`, `app/main.py`, `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/templates/index.html`
- **Ghi chú/Test (nếu có):** Test trực tiếp: roots 8 mounts, browse `/home/locpham` 22 dirs; serve wav → content-type audio/wav; chặn `/etc/shadow`. Smoke jsdom: click My Computer → drive root hiển thị → click drive → list `beat.wav`/`loop.mid`, address hiện path. Hard reload để test thủ công. - **Ghi chú/Test (nếu có):** Test trực tiếp: roots 8 mounts, browse `/home/locpham` 22 dirs; serve wav → content-type audio/wav; chặn `/etc/shadow`. Smoke jsdom: click My Computer → drive root hiển thị → click drive → list `beat.wav`/`loop.mid`, address hiện path. Hard reload để test thủ công.
### [2026-08-02 14:15] Task: Fix Window Explorer API không hoạt động
- **Tóm tắt thay đổi:** `app/api/v1/media.py` `list_computer_roots`: lọc `/proc/mounts` chỉ giữ filesystem thật (`REAL_FS_TYPES`: ext4/xfs/btrfs/ntfs/vfat...), bỏ pseudo/docker/systemd mounts (`/run/credentials/...`, overlay, tmpfs, proc...) trước đó list ra 8 mount rác không duyệt được. Bỏ import `windll` thừa. Frontend `MediaExplorerPanel`: bỏ `setComputerRoots([])` nuốt lỗi — giờ API fail → fallback về root `/` + `showToast` báo lỗi; `browseComputerDir` fail → toast thay vì im lặng; `goComputerParent` sửa edge case Windows root (`C:\`); `fileDuration` match theo `path` cho file local; size MIDI local hiện "MIDI TPQN".
- **Các file ảnh hưởng:** `app/api/v1/media.py`, `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`, `app/templates/index.html`
- **Ghi chú/Test (nếu có):** Test router riêng: `/computer` giờ trả đúng `/` + `/boot/efi` (bỏ rác), browse root 21 dirs; smoke jsdom: click My Computer → drive render → browse file OK; API fail → fallback root OK. Lưu ý: cần restart server FastAPI để load router mới + hard reload browser.