filebrowser/backend/http/share.go

212 lines
5.8 KiB
Go
Raw Permalink Normal View History

package http
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"sort"
"strconv"
"time"
"golang.org/x/crypto/bcrypt"
2024-12-17 00:01:55 +00:00
"github.com/gtsteffaniak/filebrowser/backend/errors"
"github.com/gtsteffaniak/filebrowser/backend/share"
)
2024-11-21 00:15:30 +00:00
// shareListHandler returns a list of all share links.
// @Summary List share links
// @Description Returns a list of share links for the current user, or all links if the user is an admin.
// @Tags Shares
// @Accept json
// @Produce json
// @Success 200 {array} share.Link "List of share links"
// @Failure 500 {object} map[string]string "Internal server error"
// @Router /api/shares [get]
func shareListHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
var (
s []*share.Link
err error
)
if d.user.Perm.Admin {
2024-11-21 00:15:30 +00:00
s, err = store.Share.All()
} else {
2024-11-21 00:15:30 +00:00
s, err = store.Share.FindByUserID(d.user.ID)
}
if err == errors.ErrNotExist {
return renderJSON(w, r, []*share.Link{})
}
if err != nil {
return http.StatusInternalServerError, err
}
sort.Slice(s, func(i, j int) bool {
if s[i].UserID != s[j].UserID {
return s[i].UserID < s[j].UserID
}
return s[i].Expire < s[j].Expire
})
return renderJSON(w, r, s)
2024-11-21 00:15:30 +00:00
}
2024-11-21 00:15:30 +00:00
// shareGetsHandler retrieves share links for a specific resource path.
// @Summary Get share links by path
// @Description Retrieves all share links associated with a specific resource path for the current user.
// @Tags Shares
// @Accept json
// @Produce json
// @Param path query string true "Resource path for which to retrieve share links"
// @Success 200 {array} share.Link "List of share links for the specified path"
// @Failure 500 {object} map[string]string "Internal server error"
// @Router /api/share [get]
func shareGetsHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
path := r.URL.Query().Get("path")
s, err := store.Share.Gets(path, d.user.ID)
if err == errors.ErrNotExist {
return renderJSON(w, r, []*share.Link{})
}
2025-01-05 19:05:33 +00:00
if err != nil {
2024-11-26 17:21:41 +00:00
return http.StatusInternalServerError, fmt.Errorf("error getting share info from server")
}
return renderJSON(w, r, s)
2024-11-21 00:15:30 +00:00
}
2024-11-21 00:15:30 +00:00
// shareDeleteHandler deletes a specific share link by its hash.
// @Summary Delete a share link
// @Description Deletes a share link specified by its hash.
// @Tags Shares
// @Accept json
// @Produce json
// @Param hash path string true "Hash of the share link to delete"
// @Success 200 "Share link deleted successfully"
// @Failure 400 {object} map[string]string "Bad request - missing or invalid hash"
// @Failure 500 {object} map[string]string "Internal server error"
// @Router /api/shares/{hash} [delete]
func shareDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
hash := r.URL.Query().Get("hash")
if hash == "" {
return http.StatusBadRequest, nil
}
2024-11-21 00:15:30 +00:00
err := store.Share.Delete(hash)
2024-02-10 00:13:02 +00:00
if err != nil {
return errToStatus(err), err
}
return errToStatus(err), err
2024-11-21 00:15:30 +00:00
}
2024-02-10 00:13:02 +00:00
2024-11-21 00:15:30 +00:00
// sharePostHandler creates a new share link.
// @Summary Create a share link
// @Description Creates a new share link with an optional expiration time and password protection.
// @Tags Shares
// @Accept json
// @Produce json
// @Param body body share.CreateBody true "Share link creation parameters"
// @Success 200 {object} share.Link "Created share link"
// @Failure 400 {object} map[string]string "Bad request - failed to decode body"
// @Failure 500 {object} map[string]string "Internal server error"
// @Router /api/shares [post]
func sharePostHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
var s *share.Link
var body share.CreateBody
if r.Body != nil {
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
return http.StatusBadRequest, fmt.Errorf("failed to decode body: %w", err)
}
defer r.Body.Close()
}
2024-11-21 00:15:30 +00:00
secure_hash, err := generateShortUUID()
if err != nil {
return http.StatusInternalServerError, err
}
var expire int64 = 0
if body.Expires != "" {
//nolint:govet
num, err := strconv.Atoi(body.Expires)
if err != nil {
return http.StatusInternalServerError, err
}
var add time.Duration
switch body.Unit {
case "seconds":
add = time.Second * time.Duration(num)
case "minutes":
add = time.Minute * time.Duration(num)
case "days":
add = time.Hour * 24 * time.Duration(num)
default:
add = time.Hour * time.Duration(num)
}
expire = time.Now().Add(add).Unix()
}
hash, status, err := getSharePasswordHash(body)
if err != nil {
return status, err
}
2024-02-10 00:13:02 +00:00
stringHash := ""
var token string
if len(hash) > 0 {
tokenBuffer := make([]byte, 24) //nolint:gomnd
if _, err := rand.Read(tokenBuffer); err != nil {
return http.StatusInternalServerError, err
}
token = base64.URLEncoding.EncodeToString(tokenBuffer)
2024-02-10 00:13:02 +00:00
stringHash = string(hash)
}
2024-11-21 00:15:30 +00:00
path := r.URL.Query().Get("path")
s = &share.Link{
2024-11-21 00:15:30 +00:00
Path: path,
Hash: secure_hash,
Expire: expire,
UserID: d.user.ID,
2024-02-10 00:13:02 +00:00
PasswordHash: stringHash,
Token: token,
}
2024-11-21 00:15:30 +00:00
if err := store.Share.Save(s); err != nil {
return http.StatusInternalServerError, err
}
return renderJSON(w, r, s)
2024-11-21 00:15:30 +00:00
}
func getSharePasswordHash(body share.CreateBody) (data []byte, statuscode int, err error) {
if body.Password == "" {
return nil, 0, nil
}
hash, err := bcrypt.GenerateFromPassword([]byte(body.Password), bcrypt.DefaultCost)
if err != nil {
2024-11-26 17:21:41 +00:00
return nil, http.StatusInternalServerError, fmt.Errorf("failed to hash password")
}
return hash, 0, nil
}
2024-11-21 00:15:30 +00:00
func generateShortUUID() (string, error) {
// Generate 16 random bytes (128 bits of entropy)
bytes := make([]byte, 16)
_, err := rand.Read(bytes)
if err != nil {
return "", err
}
// Encode the bytes to a URL-safe base64 string
uuid := base64.RawURLEncoding.EncodeToString(bytes)
// Trim the length to 22 characters for a shorter ID
return uuid[:22], nil
}