fix: Window Explorer liệt kê ổ đĩa thật + xử lý lỗi API
This commit is contained in:
+43
-10
@@ -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
@@ -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 tới đỉ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
@@ -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 {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user