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" "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) } // 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 deletedByAdmin := false if user.IsAdmin { if object.AssociatedUser != nil && *object.AssociatedUser != user.ID { deleteReason = adminDeleteReason deletedByAdmin = true } if delReason := r.URL.Query().Get("reason"); delReason != "" { deleteReason = delReason } } // Mark as tombstone 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 { exists, err := db.CheckIfObjectExists(object.SHA256HashBytes) if err != nil { log.Error().Err(err).Str("hash", *object.SHA256Hash).Msg("failed to check if file exists") panic(apierrors.InternalServerError) } 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 = ¤tTime 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}) }