mirror of
https://github.com/profullstack/agentbbs.git
synced 2026-08-13 22:37:28 +00:00
files: /me and /public are two separate per-user areas
Per feedback: /me is PRIVATE and the public folder must be its own top-level area, not nested under /me. - A member now has two sibling areas over SFTP: /me (private, <root>/files/users/<name>) and /public (their own public files, <root>/files/public/<name>), served anonymously at ~<name>/public. - Drop the global shared /public web route and the /me/public nesting. The anon surface only exposes ~name/public; /me has no anon route. - Both owned areas count toward the quota gauge. - Index publish hint, docs, and setup.sh updated to scp :/public/. files.<host> stays a file server; member sites remain on the BBS. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
a2d4817a8e
commit
f2bcb7e063
7 changed files with 135 additions and 135 deletions
|
|
@ -93,14 +93,16 @@ func (s *Service) privRoot(user string) string {
|
|||
return filepath.Join(s.cfg.Root, "users", user)
|
||||
}
|
||||
|
||||
// pubRoot is the absolute shared public-area directory.
|
||||
// pubRoot is the parent of the per-user public areas (<root>/public). Each
|
||||
// member's own public files live in a subdirectory keyed by handle; this parent
|
||||
// is what the operator moderation pane lists.
|
||||
func (s *Service) pubRoot() string { return filepath.Join(s.cfg.Root, "public") }
|
||||
|
||||
// publicHome is the member's own public file folder: the public/ subdirectory of
|
||||
// their private home. It is exposed anonymously on the web at ~<name>/public
|
||||
// (the unix ~/public convention) and metered as part of /me.
|
||||
func (s *Service) publicHome(user string) string {
|
||||
return filepath.Join(s.privRoot(user), "public")
|
||||
// userPub is a member's own public file area: <root>/public/<name>. It is a
|
||||
// top-level area in its own right (sibling to the private /me, NOT nested under
|
||||
// it) and is served anonymously on the web at ~<name>/public.
|
||||
func (s *Service) userPub(user string) string {
|
||||
return filepath.Join(s.pubRoot(), user)
|
||||
}
|
||||
|
||||
// ensureWorkspace creates a member's private workspace if absent.
|
||||
|
|
@ -108,14 +110,24 @@ func (s *Service) ensureWorkspace(user string) error {
|
|||
return os.MkdirAll(s.privRoot(user), 0o700)
|
||||
}
|
||||
|
||||
// ensurePublicHome creates a member's ~/public folder if absent (and the home
|
||||
// above it). The home stays private (0o700); the public/ subdir is the only
|
||||
// part the web host exposes anonymously, and the Go server reads it directly.
|
||||
func (s *Service) ensurePublicHome(user string) error {
|
||||
if err := s.ensureWorkspace(user); err != nil {
|
||||
return err
|
||||
// ensureUserPub creates a member's public area if absent. It is world-readable
|
||||
// (0o755) because the web host serves it anonymously at ~<name>/public.
|
||||
func (s *Service) ensureUserPub(user string) error {
|
||||
return os.MkdirAll(s.userPub(user), 0o755)
|
||||
}
|
||||
|
||||
// ownedUsage sums a member's two owned areas — their private /me and their
|
||||
// public /public — for the quota gauge.
|
||||
func (s *Service) ownedUsage(user string) (int64, error) {
|
||||
priv, err := dirSize(s.privRoot(user))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return os.MkdirAll(s.publicHome(user), 0o755)
|
||||
pub, err := dirSize(s.userPub(user))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return priv + pub, nil
|
||||
}
|
||||
|
||||
// quotaFor returns the effective quota (bytes) for a user: their per-user
|
||||
|
|
@ -195,7 +207,7 @@ func (s *Service) Members() ([]Member, error) {
|
|||
if u.Banned {
|
||||
continue
|
||||
}
|
||||
n, _ := dirSize(s.publicHome(u.Name))
|
||||
n, _ := dirSize(s.userPub(u.Name))
|
||||
out = append(out, Member{Name: u.Name, PublicBytes: n})
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
||||
|
|
@ -203,13 +215,13 @@ func (s *Service) Members() ([]Member, error) {
|
|||
}
|
||||
|
||||
// AnonRoot resolves the on-disk root for an anonymous, read-only browse target:
|
||||
// the shared public area (name == "") or a member's public files folder, i.e.
|
||||
// ~/public (name = the ~handle). ok is false when the named member does not
|
||||
// exist or is banned. The returned root is a confinement boundary — callers must
|
||||
// safeJoin onto it, and it never exposes the rest of the member's private home.
|
||||
// a member's own public area /public (name = the ~handle), served at
|
||||
// ~<name>/public. ok is false when the named member does not exist or is banned.
|
||||
// The returned root is a confinement boundary — callers must safeJoin onto it,
|
||||
// and it never exposes the member's private /me.
|
||||
func (s *Service) AnonRoot(name string) (root string, ok bool, err error) {
|
||||
if name == "" {
|
||||
return s.pubRoot(), true, nil
|
||||
return "", false, nil
|
||||
}
|
||||
u, found, err := s.st.UserByName(name)
|
||||
if err != nil {
|
||||
|
|
@ -218,13 +230,13 @@ func (s *Service) AnonRoot(name string) (root string, ok bool, err error) {
|
|||
if !found || u.Banned {
|
||||
return "", false, nil
|
||||
}
|
||||
// Materialize the (idempotent) ~/public folder so ~name/public is browsable
|
||||
// the moment the account exists. Without this, joining onto a missing root
|
||||
// trips the escape guard.
|
||||
if err := s.ensurePublicHome(u.Name); err != nil {
|
||||
// Materialize the (idempotent) public area so ~name/public is browsable the
|
||||
// moment the account exists. Without this, joining onto a missing root trips
|
||||
// the escape guard.
|
||||
if err := s.ensureUserPub(u.Name); err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
return s.publicHome(u.Name), true, nil
|
||||
return s.userPub(u.Name), true, nil
|
||||
}
|
||||
|
||||
// SafeJoin exposes the area-confinement join (lexical + symlink-escape guard)
|
||||
|
|
@ -245,10 +257,10 @@ func (u Usage) Free() int64 {
|
|||
return u.Quota - u.Bytes
|
||||
}
|
||||
|
||||
// Usage computes a member's private-workspace usage (all of /me, which includes
|
||||
// their ~/public folder) against their quota.
|
||||
// Usage computes a member's owned-storage usage (private /me + public /public)
|
||||
// against their quota.
|
||||
func (s *Service) Usage(u store.User) (Usage, error) {
|
||||
used, err := dirSize(s.privRoot(u.Name))
|
||||
used, err := s.ownedUsage(u.Name)
|
||||
if err != nil {
|
||||
return Usage{}, err
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue