Skip to content
Snippets Groups Projects
deleteobject.go 3.38 KiB
Newer Older
package routes

import (
	"database/sql"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"owo.codes/whats-this/api/lib/apierrors"
	"owo.codes/whats-this/api/lib/db"
	"owo.codes/whats-this/api/lib/middleware"
Dean's avatar
Dean committed
	"owo.codes/whats-this/api/lib/ratelimiter"

	"github.com/go-chi/render"
	"github.com/pkg/errors"
	"github.com/rs/zerolog/log"
	"github.com/spf13/viper"
)

// Default deletion reason
const defaultDeleteReason = "deleted by user"
const adminDeleteReason = "deleted by an administrator"

// DeleteObject deletes an object if the user owns it.
func DeleteObject(w http.ResponseWriter, r *http.Request) {
	// Only authorized users can use this route
	user := middleware.GetAuthorizedUser(r)
	if user.ID == "" || user.IsBlocked {
		panic(apierrors.Unauthorized)
	}

Dean's avatar
Dean committed
	// Apply ratelimits
	bucket := middleware.GetBucket(r)
	err := bucket.TakeWithHeaders(w, viper.GetInt64("ratelimiter.deleteObjectCost"))
	if err == ratelimiter.InsufficientTokens {
		panic(apierrors.InsufficientTokens)
	}
	if err != nil {
		panic(apierrors.InternalServerError)
	}

	// Get the key
	key := r.URL.Path
	if strings.HasPrefix(key, "/objects/") {
		key = key[9:]
	}

	// Get the object
	object, err := db.GetObject(viper.GetString("database.objectBucket"), key)
	switch {
	case errors.Cause(err) == sql.ErrNoRows:
		panic(apierrors.Unauthorized)
	case err != nil:
		log.Error().Err(err).Msg("failed to get object")
		panic(apierrors.InternalServerError)
	}
	if object.Type == 2 {
		if user.IsAdmin {
			panic(apierrors.AlreadyDeleted)
		}
		panic(apierrors.Unauthorized)
	}

	// Check if user is associated with the object then delete it
	if !user.IsAdmin && (object.AssociatedUser == nil || *object.AssociatedUser != user.ID) {
		panic(apierrors.Unauthorized)
	}

	// Determine delete reason
	deleteReason := defaultDeleteReason
Dean's avatar
Dean committed
	deletedByAdmin := false
	if user.IsAdmin {
		if object.AssociatedUser != nil && *object.AssociatedUser != user.ID {
			deleteReason = adminDeleteReason
Dean's avatar
Dean committed
			deletedByAdmin = true
		}
		if delReason := r.URL.Query().Get("reason"); delReason != "" {
			deleteReason = delReason
		}
	}

	// Mark as tombstone
Dean's avatar
Dean committed
	err = db.UpdateObjectToTombstoneByBucketKey(viper.GetString("database.objectBucket"), key, &deleteReason, deletedByAdmin, false)
	if err != nil {
		log.Error().Err(err).Msg("failed to set object to tombstone")
		panic(apierrors.InternalServerError)
	}

	// Delete file from disk if type was 0
	if object.Type == 0 {
Dean's avatar
Dean committed
		exists, err := db.CheckIfObjectExists(object.SHA256HashBytes)
		if err != nil {
Dean's avatar
Dean committed
			log.Error().Err(err).Str("hash", *object.SHA256Hash).Msg("failed to check if file exists")
			panic(apierrors.InternalServerError)
		}
Dean's avatar
Dean committed

		if !exists {
			destPath := filepath.Join(viper.GetString("files.storageLocation"), *object.SHA256Hash)
			err = os.Remove(destPath)
			if err != nil {
				log.Error().Err(err).Msg("failed to delete object file from disk")
				panic(apierrors.InternalServerError)
			}
		}
	}

	// Return tombstone response
	object.Type = 2 // tombstone
	object.DestURL = nil
	object.ContentType = nil
	object.ContentLength = nil
	currentTime := time.Now().UTC()
	object.DeletedAt = &currentTime
	object.DeleteReason = &deleteReason
	object.AssociatedUser = nil
	object.MD5Hash = nil
	associatedWithCurrentUser := false
	object.AssociatedWithCurrentUser = &associatedWithCurrentUser
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	render.JSON(w, r, objectResponse{true, object})
}