init
This commit is contained in:
commit
49b24e5773
22 changed files with 1499 additions and 0 deletions
48
web/logger.go
Normal file
48
web/logger.go
Normal 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
49
web/show_file.go
Normal 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
103
web/upload_file.go
Normal 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
63
web/web.go
Normal 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")
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue