diff --git a/chfs-ng/internal/api/server.go b/chfs-ng/internal/api/server.go index 9db8e81..32407ca 100644 --- a/chfs-ng/internal/api/server.go +++ b/chfs-ng/internal/api/server.go @@ -359,7 +359,7 @@ func (s *Server) transfer(w http.ResponseWriter, r *http.Request, copyOnly bool) writeJSONError(w, http.StatusBadRequest, "invalid parameter") return } - if !a.MaskFor(src).HasRead() || !a.MaskFor(dst).HasWrite() || !a.MaskFor(path.Dir(dst)).HasWrite() { + if !a.MaskFor(src).HasRead() || (!copyOnly && !a.MaskFor(src).HasDelete()) || !a.MaskFor(dst).HasWrite() || !a.MaskFor(path.Dir(dst)).HasWrite() { writeJSONError(w, http.StatusForbidden, "has no privilege") return } @@ -384,6 +384,12 @@ func (s *Server) transfer(w http.ResponseWriter, r *http.Request, copyOnly bool) writeJSONError(w, http.StatusForbidden, err.Error()) return } + if !copyOnly { + if err = checkDeleteTree(s.Store, a, src); err != nil { + writeJSONError(w, http.StatusForbidden, err.Error()) + return + } + } } if copyOnly { err = s.Store.Copy(src, dst) diff --git a/chfs-ng/internal/api/web/index.html b/chfs-ng/internal/api/web/index.html index 2d4aea4..49d9230 100644 --- a/chfs-ng/internal/api/web/index.html +++ b/chfs-ng/internal/api/web/index.html @@ -20,7 +20,7 @@ html.dark{--bg:#0f141c;--surface:#171e28;--surface2:#202936;--text:#e7edf7;--mut body{background:var(--bg);color:var(--text);transition:background .2s,color .2s}.top,.card,.dialog{background:var(--surface);border-color:var(--border)} button.ghost{background:var(--surface2);color:var(--text)}button.danger{background:var(--danger)}.meta,.path{color:var(--muted)} .card{box-shadow:var(--shadow)}th{background:var(--surface2);color:var(--muted)}th,td{border-color:var(--border)}.field input,.toolbar input,.field select{background:var(--surface);color:var(--text);border-color:var(--border)} -.iconbtn{width:38px;height:38px;padding:0;background:var(--surface2);color:var(--text)}.viewbtn.active{background:var(--primary);color:#fff}.selectbox{width:18px;height:18px;accent-color:var(--primary)} +.iconbtn{width:38px;height:38px;padding:0;background:var(--surface2);color:var(--text);display:inline-flex;align-items:center;justify-content:center}.ui-icon{width:18px;height:18px;display:block;stroke:currentColor;fill:none;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}.ui-icon.fill{fill:currentColor;stroke:none}.viewbtn.active{background:var(--primary);color:#fff}.selectbox{width:18px;height:18px;accent-color:var(--primary)} .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:12px;padding:14px}.tile{position:relative;border:1px solid var(--border);border-radius:12px;padding:14px;background:var(--surface);min-height:145px;cursor:pointer}.tile:hover{border-color:var(--primary)}.tile.selected{outline:2px solid var(--primary)}.tile .thumb{height:82px;display:flex;align-items:center;justify-content:center;font-size:42px;border-radius:9px;background:var(--surface2);overflow:hidden;margin-bottom:9px}.tile .thumb img{width:100%;height:100%;object-fit:cover}.progress{height:8px;background:var(--surface2);border-radius:99px;overflow:hidden}.progress i{display:block;height:100%;width:0;background:var(--primary);transition:width .15s}.upload-row{display:grid;gap:7px;padding:10px 0;border-bottom:1px solid var(--border)}.drop-active{border-color:var(--primary)!important;background:var(--surface2)!important}.qr{width:min(320px,80vw);aspect-ratio:1;margin:auto;display:block;border-radius:12px;background:#fff}.drop-hint{color:var(--muted);font-size:13px;margin-top:7px}.tile .tilecheck{position:absolute;top:9px;left:9px}.tile .tilemenu{position:absolute;right:7px;top:7px}.file-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.bulk{display:none;align-items:center;gap:8px;margin-bottom:12px}.bulk.show{display:flex}.bulk .count{font-weight:650;margin-right:auto}.layout{display:grid;grid-template-columns:210px minmax(0,1fr);gap:16px}.sidebar{background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:12px;height:max-content;position:sticky;top:82px}.side-title{font-size:12px;font-weight:700;color:var(--muted);padding:6px 9px}.side-item{width:100%;text-align:left;background:transparent;color:var(--text);margin:2px 0}.side-item.active{background:var(--surface2);color:var(--primary)}@media(max-width:900px){.layout{display:block}.sidebar{position:static;margin-bottom:14px;display:flex;gap:6px;overflow:auto}.side-title{display:none}.side-item{white-space:nowrap;width:auto}}.toast-host{position:fixed;right:18px;bottom:18px;z-index:20;display:grid;gap:8px}.toast{min-width:240px;max-width:min(420px,calc(100vw - 36px));padding:11px 14px;border:1px solid var(--border);border-radius:11px;background:var(--surface);box-shadow:var(--shadow);color:var(--text);animation:toastIn .18s ease}@keyframes toastIn{from{transform:translateY(8px);opacity:0}to{transform:none;opacity:1}}.toast.error{border-color:var(--danger)}.context{position:fixed;z-index:10;display:none;min-width:170px;background:var(--surface);border:1px solid var(--border);box-shadow:var(--shadow);border-radius:10px;padding:5px}.context button{display:block;width:100%;text-align:left;background:transparent;color:var(--text);border-radius:7px}.context button:hover{background:var(--surface2)} @@ -39,7 +39,7 @@ button.ghost{background:var(--surface2);color:var(--text)}button.danger{backgrou - +
名称大小修改时间
@@ -53,7 +53,7 @@ async function init(){try{me=await api("/chfs/me")}catch{me={username:"",home:"" function updateUser(){$("who").textContent=me.username?me.username: "访客";$("loginBtn").hidden=!!me.username;$("logoutBtn").hidden=!me.username;$("usersBtn").hidden=!me.admin;$("pwdBtn").hidden=!me.username||!me.managed} async function refresh(){try{const q=encodeURIComponent(current);const data=await api("/chfs/files?filepath="+q);currentMask=data.currentMask||"";const writable=currentMask.includes("W");$("newFolderBtn").disabled=!writable;$("newTextBtn").disabled=!writable;$("uploadBtn").disabled=!writable;$("search").disabled=!data.currentMask?.includes("R");render(data.files||[]);loadSideRoots();renderBreadcrumb();$("notice").innerHTML=""}catch(e){$("notice").innerHTML='
'+esc(e.message)+"
"}} function fileType(f){const n=f.name.toLowerCase();if(f.dir)return "folder";if(/\.(png|jpe?g|gif|webp|bmp|svg)$/.test(n))return "image";if(/\.(mp4|webm|mov|mkv)$/.test(n))return "video";if(/\.(mp3|wav|flac|m4a|ogg)$/.test(n))return "audio";if(/\.(pdf)$/.test(n))return "pdf";return "file"} -function iconFor(f){return {folder:"📁",image:"🖼️",video:"🎬",audio:"🎵",pdf:"📕",file:"📄"}[fileType(f)]} +function svgIcon(type){const p={folder:'',image:'',video:'',audio:'',pdf:'',archive:'',document:'',code:'',spreadsheet:'',presentation:'',file:''};return ''}function iconFor(f){const n=f.name.toLowerCase();const t=fileType(f);if(t!=="file")return svgIcon(t);if(/\.(zip|7z|rar|tar|gz|bz2|xz)$/.test(n))return svgIcon("archive");if(/\.(xls|xlsx|ods)$/.test(n))return svgIcon("spreadsheet");if(/\.(ppt|pptx|odp)$/.test(n))return svgIcon("presentation");if(/\.(doc|docx|odt|rtf)$/.test(n))return svgIcon("document");if(/\.(js|ts|jsx|tsx|css|scss|html|htm|go|rs|py|java|c|cpp|h|hpp|sh|sql|json|yaml|yml|xml)$/.test(n))return svgIcon("code");return svgIcon("file")} function selectItem(p,checked){if(checked)selected.add(p);else selected.delete(p);updateSelection()} let selected=new Set(),lastItems=[]; function render(items){lastItems=items;selected=new Set([...selected].filter(p=>items.some(f=>(current?current+"/":"")+f.name===p)));renderList(items);renderGrid(items);updateSelection()} @@ -75,11 +75,11 @@ async function bulkCopy(){await bulkTransfer(true)} async function bulkTransfer(copyOnly){if(!selected.size)return;const dstDir=await chooseDestination(copyOnly?"复制选中项目到":"移动选中项目到");if(dstDir===null)return;const base=String(dstDir).replace(/^\/+|\/+$/g,"");const failed=[];for(const source of [...selected]){const name=source.split("/").pop(),destination=base?(base+"/"+name):name;try{await api(copyOnly?"/chfs/copy":"/chfs/move",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({source,destination})})}catch(e){failed.push(name+": "+e.message)}}selected.clear();await refresh();if(failed.length)show('

部分操作失败

'+esc(failed.join("\n"))+'
')} async function bulkDelete(){if(!selected.size)return;if(!await confirmBox("批量删除","确定删除已选择的 "+selected.size+" 项?"))return;for(const p of [...selected]){try{await api('/chfs/rmfiles',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:new URLSearchParams({filepath:p})})}catch(e){fail(p+': '+e.message)}}selected.clear();refresh()} -async function loadSideRoots(){try{const d=await api("/chfs/roots"),roots=d.roots||[];$("sideRoots").innerHTML=roots.map(r=>'').join('')||'
没有可进入的位置
'}catch{}} +async function loadSideRoots(){try{const d=await api("/chfs/roots"),roots=d.roots||[];$("sideRoots").innerHTML=roots.map(r=>'').join('')||'
没有可进入的位置
'}catch{}} async function showRoots(){try{const d=await api("/chfs/roots");const roots=d.roots||[];let html='

选择位置

';for(const r of roots)html+='';if(!roots.length)html+='
没有可进入的共享位置
';html+='
';show(html)}catch(e){fail(e.message)}} function jumpRoot(p){current=p;navigationRoot=p;closeModal();refresh()} -function renderBreadcrumb(){const el=$("crumb"),parts=current?current.split("/").filter(Boolean):[];let html='';let acc='';for(let i=0;i'+esc(parts[i])+''}el.innerHTML=html} -function navigateTo(p){current=p;if(!navigationRoot||p===navigationRoot||p.startsWith(navigationRoot+'/')||p===''){refresh()}} +function renderBreadcrumb(){const el=$("crumb"),parts=current?current.split("/").filter(Boolean):[];const rootName=navigationRoot?(navigationRoot===me?.home?'我的空间':'当前位置'): '全部文件';let html='';let acc='';for(let i=0;i'+esc(parts[i])+''}el.innerHTML=html} +function navigateTo(p){if(navigationRoot){if(p!==navigationRoot&&!p.startsWith(navigationRoot+'/'))return}else if(p!==''&&!me?.admin)return;current=p;refresh()} function openDir(p){current=p;refresh()}function goUp(){if(!current||current===navigationRoot)return;let p=current.split("/");p.pop();let n=p.join("/");if(navigationRoot&&!(n===navigationRoot||n.startsWith(navigationRoot+"/")))n=navigationRoot;current=n;refresh()} function downloadFile(p){location.href="/chfs/shared/"+encodeURIComponent(p).replace(/%2F/g,"/")} function downloadDir(p){location.href="/chfs/downloaddir/"+encodeURI(p)} @@ -123,8 +123,8 @@ function removeRule(i){if(i<0||isHomeRule(window._editRules[i]))return;window._e function renderEditRules(){const shared=window._editRules.filter(r=>!isHomeRule(r));$("sharedRules").innerHTML=shared.length?shared.map(r=>{const i=window._editRules.indexOf(r);const flags=(r.deny?"禁止":"允许")+" · "+(r.mask||"无权限")+(r.childrenOnly?" · 仅子项":"");return '
'+esc(r.dir)+'
'+esc(flags)+'
'}).join(""):'
还没有共享目录权限
';$("rules").innerHTML=window._editRules.map((r,i)=>isHomeRule(r)?'':'
'+esc(r.dir)+' · '+esc((r.deny?"拒绝":"允许")+" · "+(r.mask||"禁止")+(r.childrenOnly?" · 仅子项":""))+'
').join("")} async function saveUser(u){try{await api("/chfs/users",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:u,enabled:$("ue").checked,rules:window._editRules})});showUsers()}catch(e){fail(e.message)}} async function deleteUser(u){if(!await confirmBox("删除用户","删除用户 "+u+"?用户目录不会自动删除。"))return;try{await api("/chfs/users?username="+encodeURIComponent(u),{method:"DELETE"});showUsers()}catch(e){fail(e.message)}} -function toggleTheme(){const dark=document.documentElement.classList.toggle("dark");localStorage.setItem("chfs-theme",dark?"dark":"light")} +function toggleTheme(){const dark=document.documentElement.classList.toggle("dark");localStorage.setItem("chfs-theme",dark?"dark":"light");renderChromeIcons()}function renderChromeIcons(){const dark=document.documentElement.classList.contains("dark");$("themeIcon").innerHTML=''+(dark?'':'')+'';$('listIcon').innerHTML='';$('gridIcon').innerHTML=''} if(localStorage.getItem("chfs-theme")==="dark"||(localStorage.getItem("chfs-theme")===null&&matchMedia("(prefers-color-scheme: dark)").matches))document.documentElement.classList.add("dark"); -setView(localStorage.getItem("chfs-view")||"list");setupDropZone();init(); +setView(localStorage.getItem("chfs-view")||"list");renderChromeIcons();setupDropZone();init();