package http import ( "crypto/rand" "encoding/base64" "encoding/json" "fmt" "net/http" "sort" "strconv" "time" "golang.org/x/crypto/bcrypt" "github.com/gtsteffaniak/filebrowser/backend/errors" "github.com/gtsteffaniak/filebrowser/backend/share" ) // 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 { s, err = store.Share.All() } else { 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) } // 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{}) } if err != nil { return http.StatusInternalServerError, fmt.Errorf("error getting share info from server") } return renderJSON(w, r, s) } // 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 } err := store.Share.Delete(hash) if err != nil { return errToStatus(err), err } return errToStatus(err), err } // 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() } 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 } 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) stringHash = string(hash) } path := r.URL.Query().Get("path") s = &share.Link{ Path: path, Hash: secure_hash, Expire: expire, UserID: d.user.ID, PasswordHash: stringHash, Token: token, } if err := store.Share.Save(s); err != nil { return http.StatusInternalServerError, err } return renderJSON(w, r, s) } 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 { return nil, http.StatusInternalServerError, fmt.Errorf("failed to hash password") } return hash, 0, nil } 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 }