This commit is contained in:
+75
-10
@@ -17,19 +17,36 @@ import (
|
||||
)
|
||||
|
||||
// MediaPath returns the absolute path of a media file, for a caller that
|
||||
// serves it. The name must carry an allowed image extension, and the file
|
||||
// is opened through the store's root, so a name that would escape the
|
||||
// content directory is refused rather than checked for.
|
||||
// serves it. The name is either a flat file of the media directory or
|
||||
// "avatars/<file>", the one namespace below it the route serves; anything
|
||||
// else, and a name that carries no allowed image extension, is refused
|
||||
// rather than checked for. The file is opened through the store's root, so
|
||||
// a path that would escape it cannot be named.
|
||||
func (s *Store) MediaPath(name string) (string, error) {
|
||||
if !imagefile.Allowed(name) {
|
||||
return "", fmt.Errorf("%q is not an allowed image name", name)
|
||||
cleaned := path.Clean(name)
|
||||
if cleaned == "." || cleaned == ".." || strings.HasPrefix(cleaned, "..") {
|
||||
return "", fmt.Errorf("%q is not a media name", name)
|
||||
}
|
||||
dir, base := path.Split(cleaned)
|
||||
dir = path.Clean(dir) // "." for a flat name, "avatars" for an avatar
|
||||
if base == "." || base == ".." || base == "" {
|
||||
return "", fmt.Errorf("%q is not a media name", name)
|
||||
}
|
||||
if !imagefile.Allowed(base) {
|
||||
return "", fmt.Errorf("%q is not an allowed image name", base)
|
||||
}
|
||||
rel := base
|
||||
if dir != "." {
|
||||
if dir != AvatarDirName {
|
||||
return "", fmt.Errorf("%q is not a served media path", name)
|
||||
}
|
||||
rel = path.Join(AvatarDirName, base)
|
||||
}
|
||||
base := filepath.Base(name)
|
||||
root, err := s.openRoot()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
handle, err := root.Open(path.Join(MediaDirName, base))
|
||||
handle, err := root.Open(path.Join(MediaDirName, rel))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -41,7 +58,7 @@ func (s *Store) MediaPath(name string) (string, error) {
|
||||
if !info.Mode().IsRegular() {
|
||||
return "", fmt.Errorf("%q is not a regular file", base)
|
||||
}
|
||||
return filepath.Join(s.ContentDir, MediaDirName, base), nil
|
||||
return filepath.Join(s.ContentDir, MediaDirName, rel), nil
|
||||
}
|
||||
|
||||
// StoreUpload persists an uploaded image and returns its public URL. The
|
||||
@@ -67,17 +84,65 @@ func (s *Store) StoreUpload(originalName string, data []byte) (string, error) {
|
||||
return "/media/" + name, nil
|
||||
}
|
||||
|
||||
// StoreAvatar persists an account profile photo and returns its public
|
||||
// URL, under avatars/ inside the media directory: the photo is account
|
||||
// state and not library content, so it never appears as a media tile.
|
||||
// The extension comes from the byte signature as StoreUpload does.
|
||||
func (s *Store) StoreAvatar(data []byte) (string, error) {
|
||||
ext := imagefile.Detect(data)
|
||||
if ext == "" {
|
||||
return "", fmt.Errorf("unsupported image format")
|
||||
}
|
||||
root, err := s.openRoot()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
dir := path.Join(MediaDirName, AvatarDirName)
|
||||
if err := root.MkdirAll(dir, 0o755); err != nil {
|
||||
return "", fmt.Errorf("create avatar directory: %w", err)
|
||||
}
|
||||
name := uuid.NewV4().String() + ext
|
||||
if err := atomicWriteIn(root, path.Join(dir, name), data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "/media/" + AvatarDirName + "/" + name, nil
|
||||
}
|
||||
|
||||
// DeleteMedia removes a media file by its public URL and reports whether a
|
||||
// file was removed.
|
||||
// file was removed. The URL names either a flat file of the media
|
||||
// directory or an avatar below avatars/; nothing else is accepted, and a
|
||||
// URL that names no file at all removes nothing.
|
||||
func (s *Store) DeleteMedia(url string) bool {
|
||||
if url == "" || !strings.HasPrefix(url, "/media/") {
|
||||
return false
|
||||
}
|
||||
name := path.Clean(strings.TrimPrefix(url, "/media/"))
|
||||
if name == "." || name == ".." || strings.HasPrefix(name, "..") {
|
||||
return false
|
||||
}
|
||||
dir, base := path.Split(name)
|
||||
dir = path.Clean(dir)
|
||||
var rel string
|
||||
switch {
|
||||
case dir == "." && base != "" && base != "." && base != "..":
|
||||
rel = base
|
||||
case dir == AvatarDirName && base != "" && base != "." && base != "..":
|
||||
rel = path.Join(AvatarDirName, base)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
root, err := s.openRoot()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return root.Remove(path.Join(MediaDirName, filepath.Base(url))) == nil
|
||||
target := path.Join(MediaDirName, rel)
|
||||
// Only a regular file is removed: a URL that resolves to the media
|
||||
// directory, the avatar directory or any other directory is refused,
|
||||
// so a delete can never empty a namespace.
|
||||
if info, err := root.Stat(target); err != nil || !info.Mode().IsRegular() {
|
||||
return false
|
||||
}
|
||||
return root.Remove(target) == nil
|
||||
}
|
||||
|
||||
// Media is one file in the media library.
|
||||
|
||||
Reference in New Issue
Block a user