~evanj/cms

1ef0cb8ed4bb5ac656094432063108852097a7df — Evan M Jones 5 months ago c8b1933
Feat(private files): Upload files to E3 as private.
M cms.go => cms.go +12 -0
@@ 9,6 9,7 @@ import (

	"git.sr.ht/~evanj/cms/internal/c/content"
	"git.sr.ht/~evanj/cms/internal/c/contenttype"
	"git.sr.ht/~evanj/cms/internal/c/file"
	"git.sr.ht/~evanj/cms/internal/c/hook"
	"git.sr.ht/~evanj/cms/internal/c/ping"
	"git.sr.ht/~evanj/cms/internal/c/space"


@@ 48,6 49,7 @@ type App struct {
	hook        http.Handler
	ping        http.Handler
	static      http.Handler
	file        http.Handler
}

func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {


@@ 58,6 60,9 @@ func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	}

	switch parts[1] {
	case "file":
		a.file.ServeHTTP(w, r)
		return
	case "static":
		a.static.ServeHTTP(w, r)
		return


@@ 122,6 127,7 @@ func init() {
			cacher,
			fs,
			webhook.New(log.New(w, "[cms:hook] ", 0), cacher),
			url,
		),
		contenttype: contenttype.New(
			log.New(w, "[cms:contenttype] ", 0),


@@ 148,6 154,12 @@ func init() {
			log.New(w, "[cms:static] ", 0),
			cacher,
		),
		file: file.New(
			log.New(w, "[cms:static] ", 0),
			cacher,
			fs,
			url,
		),
	}
}


M internal/c/content/content.go => internal/c/content/content.go +24 -9
@@ 7,6 7,7 @@ import (
	"io"
	"log"
	"net/http"
	"net/url"
	"strconv"
	"strings"



@@ 29,10 30,11 @@ var (

type Content struct {
	*c.Controller
	log  *log.Logger
	db   DBer
	e3   E3er
	hook Hooker
	log     *log.Logger
	db      DBer
	e3      E3er
	hook    Hooker
	baseURL string
}

type DBer interface {


@@ 56,16 58,31 @@ type Hooker interface {
	Do(space space.Space, content content.Content, ht webhook.HookType)
}

func New(log *log.Logger, db DBer, e3 E3er, hook Hooker) *Content {
func New(log *log.Logger, db DBer, e3 E3er, hook Hooker, baseURL string) *Content {
	return &Content{
		c.New(log, db),
		log,
		db,
		e3,
		hook,
		baseURL,
	}
}

func (c *Content) upload(ctx context.Context, filename string, file io.Reader) (string, error) {
	raw, err := c.e3.Upload(ctx, false, filename, file)
	if err != nil {
		return "", err
	}

	val, err := url.Parse(raw)
	if err != nil {
		return "", err
	}

	return c.baseURL + "/file" + val.Path, nil
}

func (c *Content) tree(w http.ResponseWriter, r *http.Request, spaceID, contenttypeID, contentID string) (user.User, space.Space, contenttype.ContentType, content.Content, error) {
	user, err := c.GetCookieUser(w, r)
	if err != nil {


@@ 145,8 162,7 @@ func (c *Content) create(w http.ResponseWriter, r *http.Request) {
			return
		}

		// TODO: Change public to false
		url, err := c.e3.Upload(r.Context(), true, header.Filename, file)
		url, err := c.upload(r.Context(), header.Filename, file)
		if err != nil {
			c.log.Println("failed to upload file", err)
			c.Error(w, r, http.StatusInternalServerError, "failed to upload file")


@@ 292,8 308,7 @@ func (c *Content) update(w http.ResponseWriter, r *http.Request) {
			return
		}

		// Note: All upload public by default.
		url, err := c.e3.Upload(r.Context(), true, header.Filename, file)
		url, err := c.upload(r.Context(), header.Filename, file)
		if err != nil {
			c.log.Println("failed to upload file", err)
			c.Error(w, r, http.StatusInternalServerError, "failed to upload file")

A internal/c/file/file.go => internal/c/file/file.go +54 -0
@@ 0,0 1,54 @@
package file

import (
	"context"
	"log"
	"net/http"
	"strings"

	"git.sr.ht/~evanj/cms/internal/c"
	"git.sr.ht/~evanj/cms/internal/m/user"
)

type File struct {
	*c.Controller
	log     *log.Logger
	db      DBer
	e3      E3er
	baseURL string
}

type DBer interface {
	UserGet(username, password string) (user.User, error)
	UserGetFromToken(token string) (user.User, error)
	FileExists(URL string) (bool, error)
}

type E3er interface {
	Proxy(ctx context.Context, objectURL string) ([]byte, error)
	URL() string
}

func New(log *log.Logger, db DBer, e3 E3er, baseURL string) *File {
	return &File{c.New(log, db), log, db, e3, baseURL}
}

func (f *File) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	ok, err := f.db.FileExists(f.baseURL + r.URL.Path)
	if !ok || err != nil {
		f.log.Println(err)
		f.Error(w, r, http.StatusInternalServerError, "file does not exist")
		return
	}

	full := strings.TrimRight(f.e3.URL(), "api") + strings.TrimLeft(r.URL.Path, "/file") // TODO: Cleanup, this is hacky.
	bytes, err := f.e3.Proxy(r.Context(), full)
	if err != nil {
		f.log.Println(err)
		f.Error(w, r, http.StatusInternalServerError, "failed to serve file")
		return
	}

	w.WriteHeader(http.StatusOK)
	w.Write(bytes)
}

A internal/c/file/file_test.go => internal/c/file/file_test.go +1 -0
@@ 0,0 1,1 @@
package file_test

M internal/s/db/db.go => internal/s/db/db.go +23 -0
@@ 339,3 339,26 @@ func (db *DB) EnsureSetup() error {

	return nil
}

// FileExists makes sure SOME space and content owns the file. I.E. deleted
// spaces can't server files.
func (db *DB) FileExists(URL string) (bool, error) {
	q := `
		SELECT cms_space.ID FROM cms_value_string_small 
		JOIN cms_value ON cms_value.VALUE_ID = cms_value_string_small.ID
		JOIN cms_contenttype_to_valuetype ON cms_value.CONTENTTYPE_TO_VALUETYPE_ID = cms_contenttype_to_valuetype.ID 
		JOIN cms_valuetype ON cms_contenttype_to_valuetype.VALUETYPE_ID = cms_valuetype.ID
		JOIN cms_contenttype ON cms_contenttype_to_valuetype.CONTENTTYPE_ID = cms_contenttype.ID
		JOIN cms_content ON cms_content.CONTENTTYPE_ID = cms_contenttype.ID
		JOIN cms_space ON cms_contenttype.SPACE_ID = cms_space.ID
		WHERE cms_valuetype.VALUE = ? AND cms_value_string_small.VALUE = ?
	`

	var spaceID string
	if err := db.QueryRow(q, valuetype.File, URL).Scan(&spaceID); err != nil {
		db.log.Println("FileExists", err)
		return false, err
	}

	return true, nil
}

M internal/s/db/space.go => internal/s/db/space.go +4 -4
@@ 68,15 68,15 @@ func (db *DB) spaceNew(t *sql.Tx, user user.User, name, desc string) (space.Spac
		return nil, fmt.Errorf("failed to create space")
	}

	if _, err := t.Exec(queryCreateNewUserToSpace, user.ID(), id); err != nil {
		return nil, fmt.Errorf("failed to attach space to user")
	}

	var space Space
	if err := t.QueryRow(queryFindSpaceByUserAndID, user.ID(), id).Scan(&space.SpaceID, &space.SpaceName, &space.SpaceDesc); err != nil {
		return nil, fmt.Errorf("failed to find space created")
	}

	if _, err := t.Exec(queryCreateNewUserToSpace, user.ID(), space.ID()); err != nil {
		return nil, fmt.Errorf("failed to attach space to user")
	}

	return &space, nil
}


M pkg/e3/e3.go => pkg/e3/e3.go +4 -0
@@ 36,6 36,10 @@ func WithClient(user, pass, url string, client *http.Client) *E3 {
	}
}

func (e3 *E3) URL() string {
	return e3.url
}

func (e3 *E3) Upload(ctx context.Context, public bool, filename string, file io.Reader) (string, error) {
	var requestBody bytes.Buffer
	mpwriter := multipart.NewWriter(&requestBody)