diff --git a/chfs-ng/internal/api/server.go b/chfs-ng/internal/api/server.go index 4a90428..ebf9829 100644 --- a/chfs-ng/internal/api/server.go +++ b/chfs-ng/internal/api/server.go @@ -76,6 +76,7 @@ func (s *Server) Handler() http.Handler { mux.HandleFunc("/chfs/exist", s.exist) mux.HandleFunc("/chfs/session", s.session) mux.HandleFunc("/chfs/me", s.me) + mux.HandleFunc("/chfs/roots", s.roots) mux.HandleFunc("/chfs/users", s.users) mux.HandleFunc("/chfs/password", s.password) mux.HandleFunc("/chfs/newtext", s.newtext) @@ -468,6 +469,45 @@ func (s *Server) me(w http.ResponseWriter, r *http.Request) { writeJSON(w, map[string]any{"username": a.Username, "home": a.Home, "admin": a.Admin, "mask": permString(a.MaskFor(a.Home)), "managed": a.PasswordHash != "", "enabled": a.Enabled}) } +type accessibleRoot struct { + Path string `json:"path"` + Name string `json:"name"` + Mask string `json:"mask"` + Home bool `json:"home"` +} + +func (s *Server) roots(w http.ResponseWriter, r *http.Request) { + a := account(r, s) + if a == nil || a.Username == "" { + writeJSONError(w, http.StatusUnauthorized, "not authenticated") + return + } + seen := map[string]bool{} + out := make([]accessibleRoot, 0) + if a.Admin && a.MaskFor("").HasRead() { + seen[""] = true + out = append(out, accessibleRoot{Path: "", Name: "全部文件", Mask: permString(a.MaskFor("")), Home: false}) + } + if a.Home != "" && a.MaskFor(a.Home).HasRead() { + home := strings.Trim(a.Home, "/") + seen[home] = true + out = append(out, accessibleRoot{Path: home, Name: "我的空间", Mask: permString(a.MaskFor(home)), Home: true}) + } + for _, rule := range a.Rules { + p := strings.Trim(rule.Dir, "/") + if p == "" || seen[p] || rule.Deny || strings.ContainsAny(p, "*?") { + continue + } + m := a.MaskFor(p) + if !m.HasRead() { + continue + } + seen[p] = true + out = append(out, accessibleRoot{Path: p, Name: path.Base(p), Mask: permString(m), Home: false}) + } + writeJSON(w, map[string]any{"roots": out}) +} + func (s *Server) password(w http.ResponseWriter, r *http.Request) { a := account(r, s) if a == nil || a.Username == "" { diff --git a/chfs-ng/internal/api/user_management_test.go b/chfs-ng/internal/api/user_management_test.go index e7ef9c4..c4545d3 100644 --- a/chfs-ng/internal/api/user_management_test.go +++ b/chfs-ng/internal/api/user_management_test.go @@ -131,6 +131,13 @@ func TestUserManagementAndPrivateHome(t *testing.T) { if rr.Code != http.StatusOK || !strings.Contains(rr.Body.String(), "\"currentMask\":\"R\"") { t.Fatalf("shared read rule: %d %s", rr.Code, rr.Body.String()) } + req = httptest.NewRequest(http.MethodGet, "/chfs/roots", nil) + req.AddCookie(&http.Cookie{Name: "JWT", Value: token}) + rr = httptest.NewRecorder() + h.ServeHTTP(rr, req) + if rr.Code != http.StatusOK || !strings.Contains(rr.Body.String(), "我的空间") || !strings.Contains(rr.Body.String(), "public") { + t.Fatalf("accessible roots: %d %s", rr.Code, rr.Body.String()) + } var saved []map[string]any data, err := os.ReadFile(usersFile) if err != nil { diff --git a/chfs-ng/internal/api/web/index.html b/chfs-ng/internal/api/web/index.html index ddbfcce..9a2a3b2 100644 --- a/chfs-ng/internal/api/web/index.html +++ b/chfs-ng/internal/api/web/index.html @@ -25,7 +25,7 @@ table{width:100%;border-collapse:collapse}th,td{text-align:left;padding:13px 15p
__CHFS_NOTICE__
- + @@ -43,6 +43,8 @@ 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||[]);$("crumb").textContent="/"+current;$("notice").innerHTML=""}catch(e){$("notice").innerHTML='
'+esc(e.message)+"
"}} function render(items){const body=$("files");body.innerHTML="";if(!items.length){body.innerHTML='这里还没有文件';return}for(const f of items){const p=current?current+"/"+f.name:f.name;const row=document.createElement("tr");const icon=f.dir?"📁":"📄";const share=f.guestMask&&f.guestMask.includes("R")?'':"";const canRead=(f.mask||"").includes("R"),canWrite=(f.mask||"").includes("W"),canDelete=(f.mask||"").includes("D");const actions=(canRead?(f.dir?'':''):"")+(canRead?share:"")+(canWrite?'':"")+(canDelete?'':"");row.innerHTML='
'+icon+''+ (f.dir&&canRead?''+esc(f.name)+'':''+esc(f.name)+'')+'
'+(f.dir?"—":fmt(f.size))+''+esc(f.modified||"")+'
'+actions+'
';body.appendChild(row)}} +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){alert(e.message)}} +function jumpRoot(p){current=p;closeModal();refresh()} function openDir(p){current=p;refresh()}function goUp(){if(!current)return;const root=me.home||"";if(current===root)return;let p=current.split("/");p.pop();let n=p.join("/");if(root&&!(n===root||n.startsWith(root+"/")))n=root;current=n;refresh()} function downloadFile(p){location.href="/chfs/shared/"+encodeURIComponent(p).replace(/%2F/g,"/")} function downloadDir(p){location.href="/chfs/downloaddir/"+encodeURI(p)}