FEAT: Plugins Manager - danh sach thu muc da muc (Add Directory + x xoa), scan phan loai rieng re VST/SoundFont theo dir
- Backend: plugin_dirs (list) thay vst_dir/soundfont_dir rieng — luu plugin_dirs.json, backward compat gop field cu; scan walk tung dir phan loai .vst3/.dll/.so -> vst_found, .sf2/.sf3 -> soundfonts (kem dir goc) - SoundFontAutoScanner: system_sf_dirs (list) — scan tat ca thu muc user khai bao - Frontend: PluginManagerModal — nut Add Directory (Tauri dialog / prompt fallback), moi dir co nut x xoa, nut Scan -> hien danh sach rieng re VST Instruments + SoundFonts (kem duong dan thu muc) - Tests: +2 (save/get/scan phan loai, xoa dir)
This commit is contained in:
+52
-19
@@ -38,18 +38,35 @@ def _save_plugin_dirs(dirs: dict):
|
||||
json.dump(dirs, f, indent=2)
|
||||
|
||||
def _effective_dirs() -> dict:
|
||||
"""Env/.env (Docker) là base; user dirs (file) override nếu khai báo."""
|
||||
"""Env/.env (Docker) là base; user dirs (file) override nếu khai báo.
|
||||
|
||||
plugin_dirs: list thư mục user thêm trong Plugins Manager (mỗi thư mục
|
||||
có thể chứa cả VST lẫn SoundFont — scan tự phân loại). Nếu user chưa
|
||||
khai báo → fallback env VST_DIR + SOUNDFONT_DIR.
|
||||
"""
|
||||
user = _load_plugin_dirs()
|
||||
plugin_dirs = [d for d in (user.get("plugin_dirs") or []) if d]
|
||||
vst_dir = settings.VST_DIR
|
||||
soundfont_dir = settings.SOUNDFONT_DIR
|
||||
# Backward compat: file cũ lưu vst_dir/soundfont_dir riêng → gộp vào list.
|
||||
if not plugin_dirs:
|
||||
if user.get("vst_dir"):
|
||||
plugin_dirs.append(user["vst_dir"])
|
||||
if user.get("soundfont_dir"):
|
||||
plugin_dirs.append(user["soundfont_dir"])
|
||||
if not plugin_dirs:
|
||||
plugin_dirs = [vst_dir, soundfont_dir]
|
||||
return {
|
||||
"vst_dir": user.get("vst_dir") or settings.VST_DIR,
|
||||
"soundfont_dir": user.get("soundfont_dir") or settings.SOUNDFONT_DIR,
|
||||
"vst_dir_user_set": bool(user.get("vst_dir")),
|
||||
"soundfont_dir_user_set": bool(user.get("soundfont_dir")),
|
||||
"plugin_dirs": plugin_dirs,
|
||||
"vst_dir": vst_dir,
|
||||
"soundfont_dir": soundfont_dir,
|
||||
"plugin_dirs_user_set": bool(user.get("plugin_dirs")),
|
||||
}
|
||||
|
||||
class DirsRequest(BaseModel):
|
||||
vst_dir: Optional[str] = None
|
||||
soundfont_dir: Optional[str] = None
|
||||
plugin_dirs: Optional[list] = None
|
||||
|
||||
@router.get("/dirs")
|
||||
async def get_plugin_dirs():
|
||||
@@ -59,6 +76,12 @@ async def get_plugin_dirs():
|
||||
async def save_plugin_dirs(req: DirsRequest, current_user: dict = Depends(get_current_user)):
|
||||
enforce_password_changed(current_user)
|
||||
user = _load_plugin_dirs()
|
||||
if req.plugin_dirs is not None:
|
||||
user["plugin_dirs"] = [d.strip() for d in req.plugin_dirs if d and d.strip()]
|
||||
# Xóa field cũ (đã gộp vào plugin_dirs) tránh nhầm lẫn
|
||||
user.pop("vst_dir", None)
|
||||
user.pop("soundfont_dir", None)
|
||||
else:
|
||||
if req.vst_dir is not None:
|
||||
user["vst_dir"] = req.vst_dir.strip()
|
||||
if req.soundfont_dir is not None:
|
||||
@@ -70,34 +93,43 @@ async def save_plugin_dirs(req: DirsRequest, current_user: dict = Depends(get_cu
|
||||
async def scan_plugin_dirs(background_tasks: BackgroundTasks = None,
|
||||
current_user: dict = Depends(get_current_user)):
|
||||
"""Scan các dir hiệu lực (env + user override): cập nhật catalog
|
||||
soundfont (inspector/scanner) + liệt kê VST. Trả về số lượng tìm thấy."""
|
||||
soundfont (inspector/scanner) + liệt kê VST. Trả về danh sách riêng rẽ
|
||||
VST (vst_found) + SoundFont (soundfonts) theo từng thư mục user khai báo."""
|
||||
enforce_password_changed(current_user)
|
||||
dirs = _effective_dirs()
|
||||
vst_dir = dirs["vst_dir"]
|
||||
sf_dir = dirs["soundfont_dir"]
|
||||
plugin_dirs = dirs["plugin_dirs"]
|
||||
# SoundFont: quét + inspect vào catalog (scan_once dùng dir hiệu lực)
|
||||
scanner = SoundFontAutoScanner(system_sf_dir=sf_dir, upload_sf_dir=UPLOAD_SF_DIR)
|
||||
scanner = SoundFontAutoScanner(system_sf_dirs=plugin_dirs, upload_sf_dir=UPLOAD_SF_DIR)
|
||||
if background_tasks:
|
||||
background_tasks.add_task(scanner.scan_once)
|
||||
else:
|
||||
scanner.scan_once()
|
||||
catalog = scanner.get_catalog()
|
||||
# VST: liệt kê thư mục (walk .vst3/.so)
|
||||
# VST + SoundFont: walk từng thư mục, phân loại riêng rẽ theo extension
|
||||
vst_found = []
|
||||
if os.path.isdir(vst_dir):
|
||||
for root, _dirs, files in os.walk(vst_dir):
|
||||
sf_found = []
|
||||
for d in plugin_dirs:
|
||||
if not os.path.isdir(d):
|
||||
continue
|
||||
for root, _dirs, files in os.walk(d):
|
||||
for f in files:
|
||||
if f.endswith(".vst3") or f.endswith(".so") or f.endswith(".dll"):
|
||||
low = f.lower()
|
||||
if low.endswith(".vst3") or low.endswith(".dll") or low.endswith(".so"):
|
||||
vst_found.append({"name": os.path.splitext(f)[0],
|
||||
"path": os.path.join(root, f),
|
||||
"type": "VST3" if f.endswith(".vst3") else "VST2"})
|
||||
"dir": d,
|
||||
"type": "VST3" if low.endswith(".vst3") else "VST2"})
|
||||
elif low.endswith(".sf2") or low.endswith(".sf3"):
|
||||
sf_found.append({"name": os.path.splitext(f)[0],
|
||||
"path": os.path.join(root, f),
|
||||
"dir": d})
|
||||
return {
|
||||
"success": True,
|
||||
"vst_dir": vst_dir,
|
||||
"soundfont_dir": sf_dir,
|
||||
"plugin_dirs": plugin_dirs,
|
||||
"vst_found": vst_found,
|
||||
"vst_count": len(vst_found),
|
||||
"soundfont_count": len(catalog),
|
||||
"soundfonts": sf_found,
|
||||
"soundfont_count": len(sf_found),
|
||||
}
|
||||
|
||||
_inspector = None
|
||||
@@ -107,14 +139,15 @@ def get_inspector():
|
||||
global _inspector
|
||||
if _inspector is None:
|
||||
d = _effective_dirs()
|
||||
_inspector = SoundFontInspector(system_sf_dir=d["soundfont_dir"], upload_sf_dir=UPLOAD_SF_DIR)
|
||||
_inspector = SoundFontInspector(d["plugin_dirs"][0] if d["plugin_dirs"] else settings.SOUNDFONT_DIR,
|
||||
upload_sf_dir=UPLOAD_SF_DIR)
|
||||
return _inspector
|
||||
|
||||
def get_scanner():
|
||||
global _scanner
|
||||
if _scanner is None:
|
||||
d = _effective_dirs()
|
||||
_scanner = SoundFontAutoScanner(system_sf_dir=d["soundfont_dir"], upload_sf_dir=UPLOAD_SF_DIR)
|
||||
_scanner = SoundFontAutoScanner(system_sf_dirs=d["plugin_dirs"], upload_sf_dir=UPLOAD_SF_DIR)
|
||||
_scanner.scan_once()
|
||||
return _scanner
|
||||
|
||||
|
||||
@@ -19,8 +19,11 @@ def _file_sig(path: str) -> tuple:
|
||||
|
||||
|
||||
class SoundFontAutoScanner:
|
||||
def __init__(self, system_sf_dir=SYSTEM_SF_DIR, upload_sf_dir=UPLOAD_SF_DIR):
|
||||
self.system_sf_dir = system_sf_dir
|
||||
def __init__(self, system_sf_dir=SYSTEM_SF_DIR, upload_sf_dir=UPLOAD_SF_DIR,
|
||||
system_sf_dirs=None):
|
||||
# system_sf_dirs (list) — nhiều thư mục user khai báo trong Plugins
|
||||
# Manager. Fallback system_sf_dir (env/.env) nếu list rỗng.
|
||||
self.system_sf_dirs = [d for d in (system_sf_dirs or []) if d] or [system_sf_dir]
|
||||
self.upload_sf_dir = upload_sf_dir
|
||||
self._catalog = {}
|
||||
self._lock = threading.Lock()
|
||||
@@ -50,6 +53,15 @@ class SoundFontAutoScanner:
|
||||
out.append((fname, os.path.join(directory, fname)))
|
||||
return out
|
||||
|
||||
def _all_sf_files(self) -> list:
|
||||
"""Gộp file .sf2/.sf3 từ TẤT CẢ thư mục hiệu lực (user dirs + upload)."""
|
||||
out = []
|
||||
for d in self.system_sf_dirs:
|
||||
out.extend(self._sf_files(d))
|
||||
if self.upload_sf_dir and os.path.isdir(self.upload_sf_dir):
|
||||
out.extend(self._sf_files(self.upload_sf_dir))
|
||||
return out
|
||||
|
||||
def _inspect_single(self, fname: str, full: str, inspector) -> dict:
|
||||
if fname.lower().endswith(".sf2"):
|
||||
sf_info = inspector.inspect_sf2_file(full) or {}
|
||||
@@ -68,9 +80,9 @@ class SoundFontAutoScanner:
|
||||
|
||||
def scan_once(self) -> bool:
|
||||
from app.core.soundfont_inspector import SoundFontInspector
|
||||
inspector = SoundFontInspector(self.system_sf_dir, self.upload_sf_dir)
|
||||
inspector = SoundFontInspector(self.system_sf_dirs[0], self.upload_sf_dir)
|
||||
found_new = False
|
||||
dirs = [(self.system_sf_dir, "system")]
|
||||
dirs = [(d, "system") for d in self.system_sf_dirs]
|
||||
if self.upload_sf_dir and os.path.isdir(self.upload_sf_dir):
|
||||
dirs.append((self.upload_sf_dir, "upload"))
|
||||
|
||||
|
||||
+73
-39
@@ -5284,47 +5284,52 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData }) => {
|
||||
const [localData, setLocalData] = React.useState(pluginsData);
|
||||
const [sfUploadStatus, setSfUploadStatus] = React.useState('');
|
||||
const [sfToDelete, setSfToDelete] = React.useState(null);
|
||||
const [pmVstDir, setPmVstDir] = React.useState('');
|
||||
const [pmSfDir, setPmSfDir] = React.useState('');
|
||||
const [pmDirs, setPmDirs] = React.useState([]);
|
||||
const [pmScanning, setPmScanning] = React.useState(false);
|
||||
const [pmScanResult, setPmScanResult] = React.useState('');
|
||||
const [pmScanData, setPmScanData] = React.useState(null); // { vst_found, soundfonts }
|
||||
React.useEffect(() => {
|
||||
if (isOpen) {
|
||||
window.SonicAPI.listPlugins()
|
||||
.then(data => setLocalData(data))
|
||||
.catch(() => setLocalData({ vst_instruments: [], soundfonts: [] }));
|
||||
window.SonicAPI.getPluginDirs()
|
||||
.then(d => {
|
||||
setPmVstDir(d.vst_dir || '');
|
||||
setPmSfDir(d.soundfont_dir || '');
|
||||
})
|
||||
.then(d => setPmDirs(d.plugin_dirs || []))
|
||||
.catch(() => {});
|
||||
setTimeout(() => { try { window.lucide.createIcons(); } catch(e) {} }, 50);
|
||||
}
|
||||
}, [isOpen]);
|
||||
// Folder picker: Tauri dialog (desktop) nếu có; fallback: paste path.
|
||||
const pickPluginFolder = async (which) => {
|
||||
const pickPluginFolder = async () => {
|
||||
try {
|
||||
if (window.__TAURI__ && window.__TAURI__.dialog) {
|
||||
const sel = await window.__TAURI__.dialog.open({ directory: true, multiple: false });
|
||||
if (typeof sel === 'string' && sel) {
|
||||
if (which === 'vst') setPmVstDir(sel);
|
||||
else setPmSfDir(sel);
|
||||
if (!pmDirs.includes(sel)) setPmDirs(prev => [...prev, sel]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
showToast('Desktop build: dùng nút Browse. Browser: dán đường dẫn vào ô.', 'info');
|
||||
const manual = window.prompt('Nhập đường dẫn thư mục plugin (VST / SoundFont):');
|
||||
if (manual && manual.trim()) {
|
||||
const p = manual.trim();
|
||||
if (!pmDirs.includes(p)) setPmDirs(prev => [...prev, p]);
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('Browse failed: ' + (e.message || e), 'error');
|
||||
}
|
||||
};
|
||||
const removePluginDir = (dir) => {
|
||||
setPmDirs(prev => prev.filter(d => d !== dir));
|
||||
};
|
||||
// Lưu dirs (user override) + scan cả 2 thư mục → refresh list + catalog.
|
||||
const saveAndScanDirs = async () => {
|
||||
setPmScanning(true);
|
||||
setPmScanResult('');
|
||||
setPmScanData(null);
|
||||
try {
|
||||
await window.SonicAPI.savePluginDirs({ vst_dir: pmVstDir, soundfont_dir: pmSfDir });
|
||||
await window.SonicAPI.savePluginDirs({ plugin_dirs: pmDirs });
|
||||
const scan = await window.SonicAPI.scanPluginDirs();
|
||||
setPmScanData({ vst_found: scan.vst_found || [], soundfonts: scan.soundfonts || [] });
|
||||
const data = await window.SonicAPI.listPlugins();
|
||||
setLocalData(data);
|
||||
try {
|
||||
@@ -5455,44 +5460,73 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData }) => {
|
||||
React.createElement('div', {
|
||||
className: 'pt-4 mt-4 border-t border-[#383838]'
|
||||
},
|
||||
React.createElement('h4', { className: 'text-xs font-bold text-zinc-400 mb-3 uppercase' },
|
||||
'Plugin Directories (VST / SoundFont)'),
|
||||
React.createElement('div', { className: 'flex gap-2 mb-2' },
|
||||
React.createElement('input', {
|
||||
type: 'text',
|
||||
value: pmVstDir,
|
||||
onChange: e => setPmVstDir(e.target.value),
|
||||
placeholder: 'VST directory (e.g. C:\\VSTs or /opt/daw_engine/vst3)',
|
||||
className: 'flex-1 bg-zinc-800 border border-zinc-700 rounded px-3 py-2 text-xs text-zinc-300 focus:outline-none focus:border-violet-600 font-mono'
|
||||
}),
|
||||
React.createElement('div', { className: 'flex items-center justify-between mb-2' },
|
||||
React.createElement('h4', { className: 'text-xs font-bold text-zinc-400 uppercase' },
|
||||
'Plugin Directories'),
|
||||
React.createElement('button', {
|
||||
className: 'px-3 py-2 bg-zinc-700 hover:bg-zinc-600 text-white text-xs font-semibold rounded transition shrink-0',
|
||||
title: 'Browse folder (desktop) - fallback: paste path',
|
||||
onClick: () => pickPluginFolder('vst')
|
||||
}, 'Browse...')
|
||||
className: 'px-3 py-1.5 bg-violet-800 hover:bg-violet-700 text-white text-xs font-semibold rounded transition flex items-center gap-1 shrink-0',
|
||||
title: 'Add plugin directory (VST / SoundFont)',
|
||||
onClick: pickPluginFolder
|
||||
},
|
||||
React.createElement('i', { 'data-lucide': 'plus', className: 'w-3 h-3' }), 'Add Directory')
|
||||
),
|
||||
React.createElement('div', { className: 'flex gap-2 mb-3' },
|
||||
React.createElement('input', {
|
||||
type: 'text',
|
||||
value: pmSfDir,
|
||||
onChange: e => setPmSfDir(e.target.value),
|
||||
placeholder: 'SoundFont directory (e.g. C:\\SoundFonts or /opt/daw_engine/soundfonts)',
|
||||
className: 'flex-1 bg-zinc-800 border border-zinc-700 rounded px-3 py-2 text-xs text-zinc-300 focus:outline-none focus:border-amber-600 font-mono'
|
||||
}),
|
||||
pmDirs.length === 0 &&
|
||||
React.createElement('p', { className: 'text-[10px] text-zinc-600 mb-2 italic' },
|
||||
'Chưa có thư mục nào. Nhấn Add Directory để chọn thư mục chứa VST / SoundFont.'),
|
||||
pmDirs.map((dir, idx) =>
|
||||
React.createElement('div', {
|
||||
key: 'pdir_' + idx,
|
||||
className: 'flex items-center gap-2 mb-1.5 bg-zinc-800/70 border border-zinc-700 rounded px-2 py-1.5'
|
||||
},
|
||||
React.createElement('button', {
|
||||
className: 'px-3 py-2 bg-zinc-700 hover:bg-zinc-600 text-white text-xs font-semibold rounded transition shrink-0',
|
||||
title: 'Browse folder (desktop) - fallback: paste path',
|
||||
onClick: () => pickPluginFolder('soundfont')
|
||||
}, 'Browse...')
|
||||
className: 'text-zinc-500 hover:text-red-400 transition shrink-0',
|
||||
title: 'Remove directory',
|
||||
onClick: () => removePluginDir(dir)
|
||||
},
|
||||
React.createElement('i', { 'data-lucide': 'x', className: 'w-3.5 h-3.5' })),
|
||||
React.createElement('span', {
|
||||
className: 'flex-1 text-[11px] text-zinc-300 font-mono truncate',
|
||||
title: dir
|
||||
}, dir),
|
||||
React.createElement('i', {
|
||||
'data-lucide': 'folder',
|
||||
className: 'w-3 h-3 text-zinc-600 shrink-0'
|
||||
})
|
||||
)
|
||||
),
|
||||
React.createElement('div', { className: 'flex gap-2 items-center' },
|
||||
React.createElement('div', { className: 'flex gap-2 items-center mt-2' },
|
||||
React.createElement('button', {
|
||||
className: 'px-4 py-2 bg-emerald-800 hover:bg-emerald-700 text-white text-xs font-semibold rounded transition flex items-center gap-1',
|
||||
onClick: saveAndScanDirs
|
||||
},
|
||||
React.createElement('i', { 'data-lucide': 'save', className: 'w-3 h-3' }), 'Save & Scan'),
|
||||
React.createElement('i', { 'data-lucide': 'search', className: 'w-3 h-3' }), 'Scan'),
|
||||
pmScanning && React.createElement('span', { className: 'text-[10px] text-emerald-400' }, 'Scanning...'),
|
||||
pmScanResult && React.createElement('span', { className: 'text-[10px] text-zinc-400' }, pmScanResult)
|
||||
),
|
||||
pmScanData && React.createElement('div', { className: 'mt-3 space-y-2 max-h-40 overflow-y-auto' },
|
||||
React.createElement('div', { className: 'text-[10px] font-bold text-violet-300 uppercase flex items-center gap-1' },
|
||||
React.createElement('i', { 'data-lucide': 'cpu', className: 'w-3 h-3' }),
|
||||
'VST Instruments (' + pmScanData.vst_found.length + ')'),
|
||||
pmScanData.vst_found.length === 0 ?
|
||||
React.createElement('p', { className: 'text-[10px] text-zinc-600 italic' }, 'Không tìm thấy VST.') :
|
||||
pmScanData.vst_found.map((v, i) =>
|
||||
React.createElement('div', { key: 'sv_' + i, className: 'flex items-center gap-2 text-[11px] text-zinc-300' },
|
||||
React.createElement('span', { className: 'w-16 shrink-0 text-zinc-500 font-mono text-[9px] truncate' }, v.type || 'VST'),
|
||||
React.createElement('span', { className: 'truncate' }, v.name),
|
||||
React.createElement('span', { className: 'text-[9px] text-zinc-600 font-mono truncate ml-auto' }, v.dir)
|
||||
)
|
||||
),
|
||||
React.createElement('div', { className: 'text-[10px] font-bold text-amber-300 uppercase flex items-center gap-1 mt-2' },
|
||||
React.createElement('i', { 'data-lucide': 'music', className: 'w-3 h-3' }),
|
||||
'SoundFonts (' + pmScanData.soundfonts.length + ')'),
|
||||
pmScanData.soundfonts.length === 0 ?
|
||||
React.createElement('p', { className: 'text-[10px] text-zinc-600 italic' }, 'Không tìm thấy SoundFont.') :
|
||||
pmScanData.soundfonts.map((s, i) =>
|
||||
React.createElement('div', { key: 'ss_' + i, className: 'flex items-center gap-2 text-[11px] text-zinc-300' },
|
||||
React.createElement('span', { className: 'truncate' }, s.name),
|
||||
React.createElement('span', { className: 'text-[9px] text-zinc-600 font-mono truncate ml-auto' }, s.dir)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
// Upload section (bottom of right panel)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -93,3 +93,55 @@ class TestPluginAPI:
|
||||
assert data["size_bytes"] == len(valid_content)
|
||||
elif resp.status_code == 403:
|
||||
pytest.skip("Permission denied for admin user")
|
||||
|
||||
def test_plugin_dirs_save_list(self):
|
||||
"""plugin_dirs (list) — save/get/effective + scan phân loại riêng rẽ."""
|
||||
token = get_admin_token()
|
||||
if not token:
|
||||
pytest.skip("Cannot get admin token")
|
||||
h = {"Authorization": f"Bearer {token}"}
|
||||
import tempfile
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
# 2 dir giả: 1 chứa VST, 1 chứa SoundFont
|
||||
vst_dir = os.path.join(td, "vsts")
|
||||
sf_dir = os.path.join(td, "sfs")
|
||||
os.makedirs(vst_dir)
|
||||
os.makedirs(sf_dir)
|
||||
open(os.path.join(vst_dir, "Synth1.vst3"), "w").write("x")
|
||||
open(os.path.join(sf_dir, "piano.sf2"), "w").write("x")
|
||||
# Save list
|
||||
r = client.post("/api/v1/plugins/dirs", headers=h,
|
||||
json={"plugin_dirs": [vst_dir, sf_dir]})
|
||||
assert r.status_code == 200
|
||||
assert r.json()["plugin_dirs"] == [vst_dir, sf_dir]
|
||||
# Get lại
|
||||
g = client.get("/api/v1/plugins/dirs", headers=h)
|
||||
assert g.json()["plugin_dirs"] == [vst_dir, sf_dir]
|
||||
# Scan → phân loại riêng rẽ
|
||||
s = client.post("/api/v1/plugins/scan", headers=h)
|
||||
assert s.status_code == 200
|
||||
data = s.json()
|
||||
assert any(v["name"] == "Synth1" for v in data["vst_found"])
|
||||
assert any(x["name"] == "piano" for x in data["soundfonts"])
|
||||
assert data["vst_count"] == 1
|
||||
assert data["soundfont_count"] == 1
|
||||
# Mỗi entry có dir gốc
|
||||
assert data["vst_found"][0]["dir"] == vst_dir
|
||||
assert data["soundfonts"][0]["dir"] == sf_dir
|
||||
|
||||
def test_plugin_dirs_remove(self):
|
||||
"""Xóa 1 dir khỏi list → save lại → không còn trong effective."""
|
||||
token = get_admin_token()
|
||||
if not token:
|
||||
pytest.skip("Cannot get admin token")
|
||||
h = {"Authorization": f"Bearer {token}"}
|
||||
import tempfile
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
d1 = os.path.join(td, "d1")
|
||||
d2 = os.path.join(td, "d2")
|
||||
os.makedirs(d1)
|
||||
os.makedirs(d2)
|
||||
client.post("/api/v1/plugins/dirs", headers=h, json={"plugin_dirs": [d1, d2]})
|
||||
client.post("/api/v1/plugins/dirs", headers=h, json={"plugin_dirs": [d1]})
|
||||
g = client.get("/api/v1/plugins/dirs", headers=h)
|
||||
assert g.json()["plugin_dirs"] == [d1]
|
||||
|
||||
Reference in New Issue
Block a user