This commit is contained in:
sam 2023-08-25 02:25:38 +02:00
commit 49b24e5773
Signed by: sam
GPG key ID: B4EF20DDE721CAA1
22 changed files with 1499 additions and 0 deletions

48
web/logger.go Normal file
View file

@ -0,0 +1,48 @@
package web
import (
"net/http"
"runtime/debug"
"time"
"github.com/go-chi/chi/v5/middleware"
"github.com/rs/zerolog/log"
)
func requestLogger(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
t1 := time.Now()
defer func() {
t2 := time.Now()
// Recover and record stack traces in case of a panic
if rec := recover(); rec != nil {
log.Error().
Str("type", "error").
Timestamp().
Interface("recover_info", rec).
Bytes("debug_stack", debug.Stack()).
Msg("Handler panicked")
http.Error(ww, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
// log end request
log.Info().
Timestamp().
Str("remote_ip", r.RemoteAddr).
Str("url", r.URL.Path).
Str("proto", r.Proto).
Str("method", r.Method).
Int("status", ww.Status()).
Dur("elapsed", t2.Sub(t1)).
Int64("bytes_in", r.ContentLength).
Int("bytes_out", ww.BytesWritten()).
Msg(r.Method + " " + r.URL.Path)
}()
next.ServeHTTP(ww, r)
}
return http.HandlerFunc(fn)
}

49
web/show_file.go Normal file
View file

@ -0,0 +1,49 @@
package web
import (
"database/sql"
"io"
"net/http"
"strconv"
"codeberg.org/u1f320/filer/db/queries"
"emperror.dev/errors"
"github.com/go-chi/chi/v5"
"github.com/rs/zerolog/log"
)
func (app *App) showFile(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
hash := chi.URLParam(r, "hash")
filename := chi.URLParam(r, "filename")
file, err := app.db.GetFileByName(ctx, queries.GetFileByNameParams{
Filename: filename,
Hash: hash,
})
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
http.Error(w, "File not found", http.StatusNotFound)
return
}
log.Err(err).Str("hash", hash).Str("name", filename).Msg("getting file from database")
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
data, err := app.db.Store.GetFile(file.ID)
if err != nil {
log.Err(err).Str("hash", hash).Str("name", filename).Msg("getting file data")
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
w.Header().Add("Content-Type", file.ContentType)
w.Header().Add("Content-Length", strconv.FormatInt(file.Size, 10))
_, err = io.Copy(w, data)
if err != nil {
log.Err(err).Str("hash", hash).Str("name", filename).Msg("writing file data")
return
}
}

103
web/upload_file.go Normal file
View file

@ -0,0 +1,103 @@
package web
import (
"bytes"
"crypto/sha256"
"database/sql"
"encoding/hex"
"fmt"
"io"
"net/http"
"codeberg.org/u1f320/filer/db/queries"
"emperror.dev/errors"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
)
// 500 megabytes
const maxMemory = 500_000_000
func (app *App) uploadFile(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "No token provided", http.StatusUnauthorized)
return
}
u, err := app.db.GetUserByToken(ctx, token)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
http.Error(w, "Invalid token provided", http.StatusForbidden)
return
}
}
err = r.ParseMultipartForm(maxMemory)
if err != nil {
log.Err(err).Msg("parsing multipart form data")
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
file, fileHeader, err := r.FormFile("file")
if err != nil {
log.Err(err).Msg("getting file from multipart form")
http.Error(w, "Bad request", http.StatusBadRequest)
}
buffer := new(bytes.Buffer)
_, err = io.Copy(buffer, file)
if err != nil {
log.Err(err).Msg("copying file to temporary buffer")
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
contentType := http.DetectContentType(buffer.Bytes())
hash, err := getSha256Hash(buffer.Bytes())
if err != nil {
log.Err(err).Msg("generating file hash")
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
dbfile, err := app.db.CreateFile(ctx, queries.CreateFileParams{
ID: uuid.New(),
UserID: u.ID,
Filename: fileHeader.Filename,
ContentType: contentType,
Hash: hash,
Size: int64(buffer.Len()),
})
if err != nil {
log.Err(err).Msg("creating file in database")
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
err = app.db.Store.WriteFile(dbfile.ID, buffer, contentType)
if err != nil {
log.Err(err).Msg("creating file in store")
err = app.db.DeleteFile(ctx, dbfile.ID)
if err != nil {
log.Err(err).Msg("deleting file from database")
}
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(fmt.Sprintf("/%s/%s", hash, fileHeader.Filename)))
}
func getSha256Hash(b []byte) (string, error) {
hasher := sha256.New()
_, err := hasher.Write(b)
if err != nil {
return "", err
}
return hex.EncodeToString(hasher.Sum(nil)), nil
}

63
web/web.go Normal file
View file

@ -0,0 +1,63 @@
package web
import (
"context"
"net/http"
"os"
"regexp"
"codeberg.org/u1f320/filer/db"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
"github.com/rs/zerolog/log"
)
type App struct {
chi.Router
db *db.DB
}
func New(db *db.DB) *App {
r := chi.NewMux()
r.Use(middleware.Recoverer)
r.Use(requestLogger)
r.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{"https://*", "http://*"},
AllowedMethods: []string{"GET", "OPTIONS"},
MaxAge: 300,
}))
app := &App{
Router: r,
db: db,
}
app.Get("/{hash}/{filename}", app.showFile)
app.Post("/upload", app.uploadFile)
return app
}
var portRegex = regexp.MustCompile(`^\d{2,}$`)
func (app *App) Run(ctx context.Context) {
port := os.Getenv("PORT")
if !portRegex.MatchString(port) {
log.Error().Str("port", port).Msg("$PORT is not a valid integer")
return
}
listenAddr := ":" + port
ech := make(chan error)
go func() {
ech <- http.ListenAndServe(listenAddr, app)
}()
log.Info().Str("port", port).Msg("Running server")
if err := <-ech; err != nil {
log.Err(err).Msg("Running server")
}
}