mirror of
https://github.com/profullstack/agentbbs.git
synced 2026-08-13 22:37:28 +00:00
files: add per-user public /site + anonymous web surface
Add a third storage area, /site — each member's own public root, served
unauthenticated on the web at ~<name> alongside the shared /public.
Web file host (files.<host>) is no longer a login wall:
- GET / -> directory of members' ~user sites (+ sign-in link)
- GET /~<name>/... -> anon read-only browse + clean file URLs of /site
- GET /public/... -> anon read-only browse + clean file URLs of shared
area (fixes bare /public requiring login: the old
Caddy `handle_path /public/*` never matched /public)
Login is now optional and gates only private /me + writes. The anon
surface has no route into anyone's /me and safeJoin rejects traversal.
Usage gauge now sums the member-owned areas (/me + /site) instead of
/me alone; shared /public stays operator-managed and unmetered.
Caddy: route all of files.<host> to the Go manager. Docs + tests updated
(anon download/browse, traversal confinement, /site metering).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
d1615ac817
commit
e478da905f
8 changed files with 486 additions and 53 deletions
|
|
@ -17,10 +17,16 @@ import (
|
|||
|
||||
// area names exposed at the virtual root.
|
||||
const (
|
||||
areaMe = "me"
|
||||
areaPublic = "public"
|
||||
areaMe = "me" // private, per-user workspace
|
||||
areaSite = "site" // the member's own public root (served at ~<name>)
|
||||
areaPublic = "public" // the single shared public area
|
||||
)
|
||||
|
||||
// metered reports whether writes to an area count against the member's quota.
|
||||
// The member's own areas (/me and /site) are metered; the shared /public is
|
||||
// operator-managed and unmetered.
|
||||
func metered(area string) bool { return area == areaMe || area == areaSite }
|
||||
|
||||
// errEscape is returned when a resolved path would leave its area root. It maps
|
||||
// to an SFTP permission-denied; it must never reach the client as a real path.
|
||||
var errEscape = errors.New("files: path escapes its area")
|
||||
|
|
@ -40,7 +46,10 @@ func (s *Service) newSession(u store.User) (*session, error) {
|
|||
if err := s.ensureWorkspace(u.Name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
used, err := dirSize(s.privRoot(u.Name))
|
||||
if err := s.ensureSite(u.Name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
used, err := s.ownedUsage(u.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -75,6 +84,10 @@ func (s *session) resolve(p string) (resolved, error) {
|
|||
switch seg[0] {
|
||||
case areaMe:
|
||||
areaRoot, area, writable = s.svc.privRoot(s.user.Name), areaMe, true
|
||||
case areaSite:
|
||||
// The member's own public root: they read/write it, the world reads it
|
||||
// anonymously at ~<name>.
|
||||
areaRoot, area, writable = s.svc.siteRoot(s.user.Name), areaSite, true
|
||||
case areaPublic:
|
||||
areaRoot, area, writable = s.svc.pubRoot(), areaPublic, s.pubWrite
|
||||
default:
|
||||
|
|
@ -152,7 +165,7 @@ func (s *session) entries(vpath string) ([]Entry, error) {
|
|||
return nil, err
|
||||
}
|
||||
if res.root {
|
||||
return []Entry{{Name: areaMe, IsDir: true}, {Name: areaPublic, IsDir: true}}, nil
|
||||
return []Entry{{Name: areaMe, IsDir: true}, {Name: areaSite, IsDir: true}, {Name: areaPublic, IsDir: true}}, nil
|
||||
}
|
||||
des, err := os.ReadDir(res.real)
|
||||
if err != nil {
|
||||
|
|
@ -296,8 +309,8 @@ func (s *session) webSave(vpath string, r io.Reader) (int64, error) {
|
|||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
limit := int64(-1) // public area is operator-managed, unmetered
|
||||
if res.area == areaMe {
|
||||
limit := int64(-1) // shared public area is operator-managed, unmetered
|
||||
if metered(res.area) {
|
||||
if limit = s.quota - (s.used.Load() - existing); limit < 0 {
|
||||
limit = 0
|
||||
}
|
||||
|
|
@ -311,7 +324,7 @@ func (s *session) webSave(vpath string, r io.Reader) (int64, error) {
|
|||
if cerr != nil {
|
||||
return 0, cerr
|
||||
}
|
||||
if res.area == areaMe {
|
||||
if metered(res.area) {
|
||||
s.used.Add(n - existing)
|
||||
}
|
||||
return n, nil
|
||||
|
|
@ -373,9 +386,9 @@ func (s *session) Filewrite(r *sftp.Request) (io.WriterAt, error) {
|
|||
if err != nil {
|
||||
return nil, sftpErr(err)
|
||||
}
|
||||
// The public area is operator-managed (no per-user quota); the private
|
||||
// workspace is metered.
|
||||
if res.area != areaMe {
|
||||
// The shared public area is operator-managed (no per-user quota); the
|
||||
// member's own areas (/me and /site) are metered.
|
||||
if !metered(res.area) {
|
||||
return f, nil
|
||||
}
|
||||
return "aWriter{f: f, sess: s, tracked: startSize}, nil
|
||||
|
|
@ -443,7 +456,7 @@ func (s *session) Filelist(r *sftp.Request) (sftp.ListerAt, error) {
|
|||
switch r.Method {
|
||||
case "List":
|
||||
if res.root {
|
||||
return listerAt{dirInfo(areaMe), dirInfo(areaPublic)}, nil
|
||||
return listerAt{dirInfo(areaMe), dirInfo(areaSite), dirInfo(areaPublic)}, nil
|
||||
}
|
||||
entries, err := os.ReadDir(res.real)
|
||||
if err != nil {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue