Skip to content

Commit

Permalink
Add a route for uploading avatars
Browse files Browse the repository at this point in the history
  • Loading branch information
nono committed Dec 10, 2024
1 parent 2d89420 commit dc5e768
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 1 deletion.
23 changes: 23 additions & 0 deletions docs/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -1083,6 +1083,29 @@ Authorization: Bearer oauth2-access-token
HTTP/1.1 204 No Content
```

## Avatar

### PUT /settings/avatar

This route can be used to upload the avatar for an instance.

#### Request

```http
PUT /settings/avatar HTTP/1.1
Host: alice.cozy.example.net
Authorization: Bearer token
Content-Type: image/jpeg
...
```

#### Response

```http
HTTP/1.1 204 No Content
```

## Context

### GET /settings/onboarded
Expand Down
23 changes: 23 additions & 0 deletions model/instance/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,29 @@ func (i *Instance) MakeVFS() error {
return err
}

// AvatarFS returns the hidden filesystem for storing the avatar.
func (i *Instance) AvatarFS() vfs.Avatarer {
fsURL := config.FsURL()
switch fsURL.Scheme {
case config.SchemeFile:
baseFS := afero.NewBasePathFs(afero.NewOsFs(),
path.Join(fsURL.Path, i.DirName(), vfs.ThumbsDirName))
return vfsafero.NewAvatarFs(baseFS)
case config.SchemeMem:
baseFS := vfsafero.GetMemFS(i.DomainName() + "-avatar")
return vfsafero.NewAvatarFs(baseFS)
case config.SchemeSwift, config.SchemeSwiftSecure:
switch i.SwiftLayout {
case 2:
return vfsswift.NewAvatarFsV3(config.GetSwiftConnection(), i)
default:
panic(ErrInvalidSwiftLayout)
}
default:
panic(fmt.Sprintf("instance: unknown storage provider %s", fsURL.Scheme))
}
}

// ThumbsFS returns the hidden filesystem for storing the thumbnails of the
// photos/image
func (i *Instance) ThumbsFS() vfs.Thumbser {
Expand Down
6 changes: 6 additions & 0 deletions model/vfs/vfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,12 @@ type DiskThresholder interface {
DiskQuota() int64
}

// Avatarer defines an interface to define an avatar filesystem.
type Avatarer interface {
CreateAvatar(contentType string) (io.WriteCloser, error)
ServeAvatarContent(w http.ResponseWriter, req *http.Request) error
}

// Thumbser defines an interface to define a thumbnail filesystem.
type Thumbser interface {
ThumbExists(img *FileDoc, format string) (ok bool, err error)
Expand Down
75 changes: 75 additions & 0 deletions model/vfs/vfsafero/avatar.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package vfsafero

import (
"io"
"net/http"
"os"

"github.com/cozy/cozy-stack/model/vfs"
"github.com/spf13/afero"
)

// NewAvatarFs creates a new avatar filesystem base on a afero.Fs.
func NewAvatarFs(fs afero.Fs) vfs.Avatarer {
return &avatar{fs}
}

type avatar struct {
fs afero.Fs
}

type avatarUpload struct {
afero.File
fs afero.Fs
tmpname string
}

func (u *avatarUpload) Close() error {
if err := u.File.Close(); err != nil {
_ = u.fs.Remove(u.tmpname)
return err
}
return u.fs.Rename(u.tmpname, "avatar")
}

func (a *avatar) CreateAvatar(contentType string) (io.WriteCloser, error) {
f, err := afero.TempFile(a.fs, "/", "avatar")
if err != nil {
return nil, err
}
tmpname := f.Name()
u := &avatarUpload{
File: f,
fs: a.fs,
tmpname: tmpname,
}
return u, nil
}

func (a *avatar) AvatarExists() (bool, error) {
infos, err := a.fs.Stat("avatar")
if os.IsNotExist(err) {
return false, nil
}
if err != nil {
return false, err
}
return infos.Size() > 0, nil
}

func (a *avatar) ServeAvatarContent(w http.ResponseWriter, req *http.Request) error {
s, err := a.fs.Stat("avatar")
if err != nil {
return err
}
if s.Size() == 0 {
return os.ErrInvalid
}
f, err := a.fs.Open("avatar")
if err != nil {
return err
}
defer f.Close()
http.ServeContent(w, req, "avatar", s.ModTime(), f)
return nil
}
47 changes: 47 additions & 0 deletions model/vfs/vfsswift/avatar_v3.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package vfsswift

import (
"context"
"fmt"
"io"
"net/http"

"github.com/cozy/cozy-stack/model/vfs"
"github.com/cozy/cozy-stack/pkg/prefixer"
"github.com/ncw/swift/v2"
)

// NewAvatarFsV3 creates a new avatar filesystem base on swift.
//
// This version stores the avatar in the same container as the main data
// container.
func NewAvatarFsV3(c *swift.Connection, db prefixer.Prefixer) vfs.Avatarer {
return &avatarV3{
c: c,
container: swiftV3ContainerPrefix + db.DBPrefix(),
ctx: context.Background(),
}
}

type avatarV3 struct {
c *swift.Connection
container string
ctx context.Context
}

func (a *avatarV3) CreateAvatar(contentType string) (io.WriteCloser, error) {
return a.c.ObjectCreate(a.ctx, a.container, "avatar", true, "", contentType, nil)
}

func (a *avatarV3) ServeAvatarContent(w http.ResponseWriter, req *http.Request) error {
f, o, err := a.c.ObjectOpen(a.ctx, a.container, "avatar", false, nil)
if err != nil {
return wrapSwiftErr(err)
}
defer f.Close()

w.Header().Set("Etag", fmt.Sprintf(`"%s"`, o["Etag"]))
w.Header().Set("Content-Type", o["Content-Type"])
http.ServeContent(w, req, "avatar", unixEpochZero, &backgroundSeeker{f})
return nil
}
6 changes: 6 additions & 0 deletions web/public/public.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package public

import (
"net/http"
"os"
"strings"
"time"

Expand All @@ -20,6 +21,11 @@ import (
// Avatar returns the default avatar currently.
func Avatar(c echo.Context) error {
inst := middlewares.GetInstance(c)
err := inst.AvatarFS().ServeAvatarContent(c.Response(), c.Request())
if err != os.ErrNotExist {
return err
}

switch c.QueryParam("fallback") {
case "404":
// Nothing
Expand Down
28 changes: 28 additions & 0 deletions web/settings/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
Expand Down Expand Up @@ -253,6 +254,31 @@ func isMovedError(err error) bool {
return ok && j.Code == "moved"
}

func (h *HTTPHandler) UploadAvatar(c echo.Context) error {
inst := middlewares.GetInstance(c)
if err := middlewares.AllowWholeType(c, http.MethodPut, consts.Settings); err != nil {
return err
}
header := c.Request().Header
size := c.Request().ContentLength
if size > 20_000_000 {
return jsonapi.BadRequest(errors.New("Avatar is too big"))
}
contentType := header.Get(echo.HeaderContentType)
f, err := inst.AvatarFS().CreateAvatar(contentType)
if err != nil {
return jsonapi.InternalServerError(err)
}
_, err = io.Copy(f, c.Request().Body)
if cerr := f.Close(); cerr != nil && err == nil {
err = cerr
}
if err != nil {
return jsonapi.InternalServerError(err)
}
return c.NoContent(http.StatusNoContent)
}

// Register all the `/settings` routes to the given router.
func (h *HTTPHandler) Register(router *echo.Group) {
router.GET("/disk-usage", h.diskUsage)
Expand Down Expand Up @@ -281,6 +307,8 @@ func (h *HTTPHandler) Register(router *echo.Group) {
router.PUT("/instance/sign_tos", h.updateInstanceTOS)
router.DELETE("/instance/moved_from", h.clearMovedFrom)

router.PUT("/avatar", h.UploadAvatar)

router.GET("/flags", h.getFlags)

router.GET("/sessions", h.getSessions)
Expand Down
10 changes: 9 additions & 1 deletion web/sharings/sharings.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"errors"
"net/http"
"net/url"
"os"
"strconv"
"strings"

Expand Down Expand Up @@ -841,9 +842,16 @@ func GetAvatar(c echo.Context) error {
m := s.Members[index]

// Use the local avatar
if m.Instance == "" || m.Instance == inst.PageURL("", nil) {
if m.Instance == "" {
return localAvatar(c, m)
}
if m.Instance == inst.PageURL("", nil) {
err := inst.AvatarFS().ServeAvatarContent(c.Response(), c.Request())
if err == os.ErrNotExist {
return localAvatar(c, m)
}
return err
}

// Use the public avatar from the member's instance
res, err := safehttp.DefaultClient.Get(m.Instance + "/public/avatar?fallback=404")
Expand Down

0 comments on commit dc5e768

Please sign in to comment.