V3 version with backward comp
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/vaultdrop/backend/internal/auth"
|
||||
"github.com/vaultdrop/backend/internal/service"
|
||||
"github.com/vaultdrop/backend/pkg/api"
|
||||
)
|
||||
|
||||
type DeviceHandler struct {
|
||||
placement *service.PlacementService
|
||||
}
|
||||
|
||||
func (h *DeviceHandler) List(c *gin.Context) {
|
||||
userID := c.GetString(auth.UserIDKey)
|
||||
|
||||
locations, err := h.placement.ListUserLocations(userID)
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to list devices")
|
||||
return
|
||||
}
|
||||
|
||||
type deviceResponse struct {
|
||||
ID string `json:"id"`
|
||||
DeviceName string `json:"device_name"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
resp := make([]deviceResponse, len(locations))
|
||||
for i, l := range locations {
|
||||
resp[i] = deviceResponse{
|
||||
ID: l.ID.String(),
|
||||
DeviceName: l.DeviceName,
|
||||
Role: l.Role,
|
||||
}
|
||||
}
|
||||
|
||||
api.Success(c, resp)
|
||||
}
|
||||
|
||||
func (h *DeviceHandler) Register(c *gin.Context) {
|
||||
userID := c.GetString(auth.UserIDKey)
|
||||
|
||||
var body struct {
|
||||
DeviceName string `json:"device_name" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
api.Error(c, http.StatusBadRequest, "INVALID_BODY", "device_name is required")
|
||||
return
|
||||
}
|
||||
|
||||
loc, err := h.placement.CreateDeviceLocation(userID, body.DeviceName)
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to register device")
|
||||
return
|
||||
}
|
||||
|
||||
api.Created(c, gin.H{
|
||||
"id": loc.ID.String(),
|
||||
"device_name": loc.DeviceName,
|
||||
"role": loc.Role,
|
||||
})
|
||||
}
|
||||
@@ -6,17 +6,37 @@ import (
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
File *FileHandler
|
||||
OCR *OCRHandler
|
||||
Health *HealthHandler
|
||||
Auth *auth.AuthHandler
|
||||
Resource *ResourceHandler
|
||||
OCR *OCRHandler
|
||||
Health *HealthHandler
|
||||
Auth *auth.AuthHandler
|
||||
Share *ShareHandler
|
||||
Device *DeviceHandler
|
||||
Sync *SyncHandler
|
||||
}
|
||||
|
||||
func New(fileSvc *service.FileService, ocrSvc *service.OCRService, urlSvc *service.URLService, authHandler *auth.AuthHandler, conversionSvc *service.ConversionService) *Handler {
|
||||
func New(
|
||||
resourceSvc *service.ResourceService,
|
||||
ocrSvc *service.OCRService,
|
||||
urlSvc *service.URLService,
|
||||
authHandler *auth.AuthHandler,
|
||||
conversionSvc *service.ConversionService,
|
||||
rebacSvc *service.RebacService,
|
||||
placementSvc *service.PlacementService,
|
||||
syncSvc *service.SyncService,
|
||||
) *Handler {
|
||||
return &Handler{
|
||||
File: &FileHandler{files: fileSvc, urls: urlSvc, ocr: ocrSvc, conversion: conversionSvc},
|
||||
OCR: &OCRHandler{ocr: ocrSvc, files: fileSvc},
|
||||
Resource: &ResourceHandler{
|
||||
resources: resourceSvc,
|
||||
urls: urlSvc,
|
||||
ocr: ocrSvc,
|
||||
conversion: conversionSvc,
|
||||
},
|
||||
OCR: &OCRHandler{ocr: ocrSvc, resources: resourceSvc},
|
||||
Health: &HealthHandler{ocr: ocrSvc},
|
||||
Auth: authHandler,
|
||||
Share: &ShareHandler{rebac: rebacSvc},
|
||||
Device: &DeviceHandler{placement: placementSvc},
|
||||
Sync: &SyncHandler{sync: syncSvc},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
)
|
||||
|
||||
type OCRHandler struct {
|
||||
ocr *service.OCRService
|
||||
files *service.FileService
|
||||
ocr *service.OCRService
|
||||
resources *service.ResourceService
|
||||
}
|
||||
|
||||
func (h *OCRHandler) CreateJob(c *gin.Context) {
|
||||
|
||||
@@ -8,18 +8,21 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/vaultdrop/backend/internal/auth"
|
||||
"github.com/vaultdrop/backend/internal/service"
|
||||
"github.com/vaultdrop/backend/pkg/api"
|
||||
)
|
||||
|
||||
type FileHandler struct {
|
||||
files *service.FileService
|
||||
type ResourceHandler struct {
|
||||
resources *service.ResourceService
|
||||
urls *service.URLService
|
||||
ocr *service.OCRService
|
||||
conversion *service.ConversionService
|
||||
}
|
||||
|
||||
func (h *FileHandler) Upload(c *gin.Context) {
|
||||
func (h *ResourceHandler) Upload(c *gin.Context) {
|
||||
userID := c.GetString(auth.UserIDKey)
|
||||
|
||||
form, err := c.MultipartForm()
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusBadRequest, "ERROR_PARSING", "Error while parsing multipart form")
|
||||
@@ -34,7 +37,7 @@ func (h *FileHandler) Upload(c *gin.Context) {
|
||||
|
||||
results := make([]gin.H, 0, len(files))
|
||||
for _, file := range files {
|
||||
result, err := h.files.Upload(file)
|
||||
result, err := h.resources.Upload(file, userID)
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, "UPLOAD_ERROR", err.Error())
|
||||
return
|
||||
@@ -55,23 +58,23 @@ func (h *FileHandler) Upload(c *gin.Context) {
|
||||
api.Success(c, results)
|
||||
}
|
||||
|
||||
func (h *FileHandler) List(c *gin.Context) {
|
||||
func (h *ResourceHandler) List(c *gin.Context) {
|
||||
userID := c.GetString(auth.UserIDKey)
|
||||
thumbnailQuality := c.Query("thumbnail")
|
||||
|
||||
files, err := h.files.List()
|
||||
resources, err := h.resources.List(userID)
|
||||
if err != nil {
|
||||
log.Printf("ERROR List files: %v", err)
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to list files")
|
||||
log.Printf("ERROR List resources: %v", err)
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to list resources")
|
||||
return
|
||||
}
|
||||
|
||||
type tagResponse struct {
|
||||
ID string `json:"id"`
|
||||
TagName string `json:"tag_name"`
|
||||
TagType string `json:"tag_type"`
|
||||
}
|
||||
|
||||
type fileResponse struct {
|
||||
type resourceResponse struct {
|
||||
ID string `json:"id"`
|
||||
URL string `json:"url"`
|
||||
ThumbnailURL string `json:"thumbnailUrl,omitempty"`
|
||||
@@ -81,50 +84,51 @@ func (h *FileHandler) List(c *gin.Context) {
|
||||
CreatedAt string `json:"createdAt"`
|
||||
MimeType string `json:"mimeType"`
|
||||
OcrText string `json:"ocrText,omitempty"`
|
||||
ParentFileID string `json:"parentFileID,omitempty"`
|
||||
ParentID string `json:"parentResourceId,omitempty"`
|
||||
IsFolder bool `json:"isFolder"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
OwnerID string `json:"ownerId"`
|
||||
}
|
||||
|
||||
resp := make([]fileResponse, len(files))
|
||||
for i, f := range files {
|
||||
resp := make([]resourceResponse, len(resources))
|
||||
for i, r := range resources {
|
||||
tags := []tagResponse{}
|
||||
for _, tag := range f.Tags {
|
||||
for _, tag := range r.Tags {
|
||||
tags = append(tags, tagResponse{
|
||||
ID: tag.ID,
|
||||
TagName: tag.Name,
|
||||
TagType: tag.TagType,
|
||||
})
|
||||
}
|
||||
|
||||
downloadURL := h.urls.GenerateDownloadURL(f.ID)
|
||||
downloadURL := h.urls.GenerateDownloadURL(r.ID)
|
||||
thumbURL := downloadURL
|
||||
if thumbnailQuality != "" {
|
||||
if best := h.files.GetBestThumbnail(f.ID, thumbnailQuality); best != nil {
|
||||
thumbURL = h.urls.GenerateThumbnailURL(best.ID)
|
||||
if best := h.resources.GetBestVariant(r.ID, thumbnailQuality); best != nil {
|
||||
thumbURL = h.urls.GenerateVariantURL(best.ID)
|
||||
}
|
||||
}
|
||||
|
||||
resp[i] = fileResponse{
|
||||
ID: f.ID,
|
||||
resp[i] = resourceResponse{
|
||||
ID: r.ID,
|
||||
URL: downloadURL,
|
||||
ThumbnailURL: thumbURL,
|
||||
Name: f.Name,
|
||||
Size: f.Size,
|
||||
Name: r.Name,
|
||||
Size: r.Size,
|
||||
Tags: tags,
|
||||
CreatedAt: f.CreatedAt,
|
||||
ParentFileID: f.ParentFileID,
|
||||
OcrText: f.OcrText,
|
||||
IsFolder: f.IsFolder,
|
||||
UpdatedAt: f.UpdatedAt,
|
||||
MimeType: f.MimeType,
|
||||
CreatedAt: r.CreatedAt.String(),
|
||||
ParentID: r.ParentResourceID,
|
||||
OcrText: r.OcrText,
|
||||
IsFolder: r.IsFolder,
|
||||
UpdatedAt: r.UpdatedAt.String(),
|
||||
MimeType: r.MimeType,
|
||||
OwnerID: r.OwnerID,
|
||||
}
|
||||
}
|
||||
|
||||
api.Paginated(c, resp, 1, len(resp))
|
||||
}
|
||||
|
||||
func (h *FileHandler) Download(c *gin.Context) {
|
||||
func (h *ResourceHandler) Download(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
exp, _ := strconv.ParseInt(c.Query("expires"), 10, 64)
|
||||
sig := c.Query("sig")
|
||||
@@ -134,133 +138,126 @@ func (h *FileHandler) Download(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
storagePath, err := h.files.GetStoragePath(id)
|
||||
storagePath, err := h.resources.GetStoragePath(id)
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusNotFound, "FILE_NOT_FOUND", "File not found")
|
||||
api.Error(c, http.StatusNotFound, "RESOURCE_NOT_FOUND", "Resource not found")
|
||||
return
|
||||
}
|
||||
|
||||
c.File(path.Clean(storagePath))
|
||||
}
|
||||
|
||||
func (h *FileHandler) Get(c *gin.Context) {
|
||||
func (h *ResourceHandler) Get(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
thumbnailQuality := c.Query("thumbnail")
|
||||
|
||||
file, err := h.files.Get(id)
|
||||
resource, err := h.resources.Get(id)
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusNotFound, "FILE_NOT_FOUND", "File not found")
|
||||
api.Error(c, http.StatusNotFound, "RESOURCE_NOT_FOUND", "Resource not found")
|
||||
return
|
||||
}
|
||||
|
||||
type tagResponse struct {
|
||||
ID string `json:"id"`
|
||||
TagName string `json:"tag_name"`
|
||||
TagType string `json:"tag_type"`
|
||||
}
|
||||
|
||||
type thumbnailResponse struct {
|
||||
ID string `json:"id"`
|
||||
PageNumber int `json:"pageNumber"`
|
||||
ResolutionLabel string `json:"resolutionLabel"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
URL string `json:"url"`
|
||||
MimeType string `json:"mimeType"`
|
||||
type variantResponse struct {
|
||||
ID string `json:"id"`
|
||||
PageNumber int `json:"pageNumber"`
|
||||
VariantType string `json:"variantType"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
URL string `json:"url"`
|
||||
MimeType string `json:"mimeType"`
|
||||
}
|
||||
|
||||
tags := []tagResponse{}
|
||||
for _, tag := range file.Tags {
|
||||
for _, tag := range resource.Tags {
|
||||
tags = append(tags, tagResponse{
|
||||
ID: tag.ID,
|
||||
TagName: tag.Name,
|
||||
TagType: tag.TagType,
|
||||
})
|
||||
}
|
||||
|
||||
downloadURL := h.urls.GenerateDownloadURL(file.ID)
|
||||
downloadURL := h.urls.GenerateDownloadURL(resource.ID)
|
||||
thumbURL := downloadURL
|
||||
if thumbnailQuality != "" {
|
||||
if best := h.files.GetBestThumbnail(file.ID, thumbnailQuality); best != nil {
|
||||
thumbURL = h.urls.GenerateThumbnailURL(best.ID)
|
||||
if best := h.resources.GetBestVariant(resource.ID, thumbnailQuality); best != nil {
|
||||
thumbURL = h.urls.GenerateVariantURL(best.ID)
|
||||
}
|
||||
}
|
||||
|
||||
dbThumbnails, _ := h.files.GetThumbnailsByFileID(file.ID)
|
||||
thumbnails := make([]thumbnailResponse, len(dbThumbnails))
|
||||
for i, t := range dbThumbnails {
|
||||
thumbnails[i] = thumbnailResponse{
|
||||
ID: t.ID,
|
||||
PageNumber: t.PageNumber,
|
||||
ResolutionLabel: t.ResolutionLabel,
|
||||
Width: t.Width,
|
||||
Height: t.Height,
|
||||
URL: h.urls.GenerateThumbnailURL(t.ID),
|
||||
MimeType: t.MimeType,
|
||||
dbVariants, _ := h.resources.GetVariantsByResourceID(resource.ID)
|
||||
variants := make([]variantResponse, len(dbVariants))
|
||||
for i, v := range dbVariants {
|
||||
variants[i] = variantResponse{
|
||||
ID: v.ID,
|
||||
PageNumber: v.PageNumber,
|
||||
VariantType: v.VariantType,
|
||||
Width: v.Width,
|
||||
Height: v.Height,
|
||||
URL: h.urls.GenerateVariantURL(v.ID),
|
||||
MimeType: v.MimeType,
|
||||
}
|
||||
}
|
||||
|
||||
api.Success(c, gin.H{
|
||||
"id": file.ID,
|
||||
"name": file.Name,
|
||||
"id": resource.ID,
|
||||
"name": resource.Name,
|
||||
"url": downloadURL,
|
||||
"thumbnailUrl": thumbURL,
|
||||
"size": file.Size,
|
||||
"mimeType": file.MimeType,
|
||||
"size": resource.Size,
|
||||
"mimeType": resource.MimeType,
|
||||
"tags": tags,
|
||||
"createdAt": file.CreatedAt,
|
||||
"updatedAt": file.UpdatedAt,
|
||||
"ocrText": file.OcrText,
|
||||
"isFolder": file.IsFolder,
|
||||
"parentFileID": file.ParentFileID,
|
||||
"thumbnails": thumbnails,
|
||||
"createdAt": resource.CreatedAt,
|
||||
"updatedAt": resource.UpdatedAt,
|
||||
"ocrText": resource.OcrText,
|
||||
"isFolder": resource.IsFolder,
|
||||
"parentResourceId": resource.ParentResourceID,
|
||||
"ownerId": resource.OwnerID,
|
||||
"variants": variants,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *FileHandler) Delete(c *gin.Context) {
|
||||
func (h *ResourceHandler) Delete(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
storagePath, _ := h.files.GetStoragePath(id)
|
||||
thumbnails, _ := h.files.GetThumbnailsByFileID(id)
|
||||
storagePath, _ := h.resources.GetStoragePath(id)
|
||||
variants, _ := h.resources.GetVariantsByResourceID(id)
|
||||
|
||||
if err := h.files.Delete(id); err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to delete file")
|
||||
if err := h.resources.Delete(id); err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to delete resource")
|
||||
return
|
||||
}
|
||||
|
||||
if storagePath != "" {
|
||||
os.Remove(path.Clean(storagePath))
|
||||
}
|
||||
for _, t := range thumbnails {
|
||||
os.Remove(path.Clean(t.StorageKey))
|
||||
for _, v := range variants {
|
||||
os.Remove(path.Clean(v.StorageKey))
|
||||
}
|
||||
|
||||
api.Success(c, gin.H{"deleted": true})
|
||||
}
|
||||
|
||||
func (h *FileHandler) AddTags(c *gin.Context) {
|
||||
func (h *ResourceHandler) AddTags(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
var body struct {
|
||||
Tags []string `json:"tags" binding:"required"`
|
||||
TagType string `json:"tag_type"`
|
||||
Tags []string `json:"tags" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
api.Error(c, http.StatusBadRequest, "INVALID_BODY", "Body must contain a 'tags' array")
|
||||
return
|
||||
}
|
||||
|
||||
tagType := body.TagType
|
||||
if tagType == "" {
|
||||
tagType = "none"
|
||||
}
|
||||
|
||||
if err := h.files.AddTags(id, body.Tags, tagType); err != nil {
|
||||
if err := h.resources.AddTags(id, body.Tags); err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to add tags")
|
||||
return
|
||||
}
|
||||
|
||||
tags, err := h.files.GetTagsByFileID(id)
|
||||
tags, err := h.resources.GetTagsByResourceID(id)
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to fetch tags")
|
||||
return
|
||||
@@ -269,9 +266,9 @@ func (h *FileHandler) AddTags(c *gin.Context) {
|
||||
api.Success(c, tags)
|
||||
}
|
||||
|
||||
func (h *FileHandler) GetTags(c *gin.Context) {
|
||||
func (h *ResourceHandler) GetTags(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
tags, err := h.files.GetTagsByFileID(id)
|
||||
tags, err := h.resources.GetTagsByResourceID(id)
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to fetch tags")
|
||||
return
|
||||
@@ -279,26 +276,27 @@ func (h *FileHandler) GetTags(c *gin.Context) {
|
||||
api.Success(c, tags)
|
||||
}
|
||||
|
||||
func (h *FileHandler) MoveFiles(c *gin.Context) {
|
||||
func (h *ResourceHandler) MoveResources(c *gin.Context) {
|
||||
var body struct {
|
||||
FileIDs []string `json:"file_ids" binding:"required"`
|
||||
ParentFileID *string `json:"parent_file_id"`
|
||||
ResourceIDs []string `json:"resource_ids" binding:"required"`
|
||||
ParentResourceID *string `json:"parent_resource_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
api.Error(c, http.StatusBadRequest, "INVALID_BODY", "Body must contain 'file_ids' array")
|
||||
api.Error(c, http.StatusBadRequest, "INVALID_BODY", "Body must contain 'resource_ids' array")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.files.MoveFiles(body.FileIDs, body.ParentFileID); err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to move files")
|
||||
if err := h.resources.MoveResources(body.ResourceIDs, body.ParentResourceID); err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to move resources")
|
||||
return
|
||||
}
|
||||
|
||||
api.Success(c, gin.H{"moved": len(body.FileIDs)})
|
||||
api.Success(c, gin.H{"moved": len(body.ResourceIDs)})
|
||||
}
|
||||
|
||||
func (h *FileHandler) ListFolders(c *gin.Context) {
|
||||
folders, err := h.files.ListFolders()
|
||||
func (h *ResourceHandler) ListFolders(c *gin.Context) {
|
||||
userID := c.GetString(auth.UserIDKey)
|
||||
folders, err := h.resources.ListFolders(userID)
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to list folders")
|
||||
return
|
||||
@@ -306,7 +304,9 @@ func (h *FileHandler) ListFolders(c *gin.Context) {
|
||||
api.Success(c, folders)
|
||||
}
|
||||
|
||||
func (h *FileHandler) CreateFolder(c *gin.Context) {
|
||||
func (h *ResourceHandler) CreateFolder(c *gin.Context) {
|
||||
userID := c.GetString(auth.UserIDKey)
|
||||
|
||||
var body struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
}
|
||||
@@ -315,7 +315,7 @@ func (h *FileHandler) CreateFolder(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
folder, err := h.files.CreateFolder(body.Name)
|
||||
folder, err := h.resources.CreateFolder(body.Name, userID)
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to create folder")
|
||||
return
|
||||
@@ -324,23 +324,23 @@ func (h *FileHandler) CreateFolder(c *gin.Context) {
|
||||
api.Success(c, folder)
|
||||
}
|
||||
|
||||
func (h *FileHandler) ListFilesByParent(c *gin.Context) {
|
||||
func (h *ResourceHandler) ListByParent(c *gin.Context) {
|
||||
userID := c.GetString(auth.UserIDKey)
|
||||
parentID := c.Param("id")
|
||||
thumbnailQuality := c.Query("thumbnail")
|
||||
|
||||
files, err := h.files.ListFilesByParentID(parentID)
|
||||
resources, err := h.resources.ListResourcesByParentID(parentID, userID)
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to list files in folder")
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to list resources in folder")
|
||||
return
|
||||
}
|
||||
|
||||
type tagResponse struct {
|
||||
ID string `json:"id"`
|
||||
TagName string `json:"tag_name"`
|
||||
TagType string `json:"tag_type"`
|
||||
}
|
||||
|
||||
type fileResponse struct {
|
||||
type resourceResponse struct {
|
||||
ID string `json:"id"`
|
||||
URL string `json:"url"`
|
||||
ThumbnailURL string `json:"thumbnailUrl,omitempty"`
|
||||
@@ -350,84 +350,83 @@ func (h *FileHandler) ListFilesByParent(c *gin.Context) {
|
||||
CreatedAt string `json:"createdAt"`
|
||||
MimeType string `json:"mimeType"`
|
||||
OcrText string `json:"ocrText,omitempty"`
|
||||
ParentFileID string `json:"parentFileID,omitempty"`
|
||||
ParentID string `json:"parentResourceId,omitempty"`
|
||||
IsFolder bool `json:"isFolder"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
resp := make([]fileResponse, len(files))
|
||||
for i, f := range files {
|
||||
resp := make([]resourceResponse, len(resources))
|
||||
for i, r := range resources {
|
||||
tags := []tagResponse{}
|
||||
for _, tag := range f.Tags {
|
||||
for _, tag := range r.Tags {
|
||||
tags = append(tags, tagResponse{
|
||||
ID: tag.ID,
|
||||
TagName: tag.Name,
|
||||
TagType: tag.TagType,
|
||||
})
|
||||
}
|
||||
|
||||
downloadURL := h.urls.GenerateDownloadURL(f.ID)
|
||||
downloadURL := h.urls.GenerateDownloadURL(r.ID)
|
||||
thumbURL := downloadURL
|
||||
if thumbnailQuality != "" {
|
||||
if best := h.files.GetBestThumbnail(f.ID, thumbnailQuality); best != nil {
|
||||
thumbURL = h.urls.GenerateThumbnailURL(best.ID)
|
||||
if best := h.resources.GetBestVariant(r.ID, thumbnailQuality); best != nil {
|
||||
thumbURL = h.urls.GenerateVariantURL(best.ID)
|
||||
}
|
||||
}
|
||||
|
||||
resp[i] = fileResponse{
|
||||
ID: f.ID,
|
||||
resp[i] = resourceResponse{
|
||||
ID: r.ID,
|
||||
URL: downloadURL,
|
||||
ThumbnailURL: thumbURL,
|
||||
Name: f.Name,
|
||||
Size: f.Size,
|
||||
Name: r.Name,
|
||||
Size: r.Size,
|
||||
Tags: tags,
|
||||
CreatedAt: f.CreatedAt,
|
||||
ParentFileID: f.ParentFileID,
|
||||
OcrText: f.OcrText,
|
||||
IsFolder: f.IsFolder,
|
||||
UpdatedAt: f.UpdatedAt,
|
||||
MimeType: f.MimeType,
|
||||
CreatedAt: r.CreatedAt.String(),
|
||||
ParentID: r.ParentResourceID,
|
||||
OcrText: r.OcrText,
|
||||
IsFolder: r.IsFolder,
|
||||
UpdatedAt: r.UpdatedAt.String(),
|
||||
MimeType: r.MimeType,
|
||||
}
|
||||
}
|
||||
|
||||
api.Success(c, resp)
|
||||
}
|
||||
|
||||
func (h *FileHandler) GetThumbnails(c *gin.Context) {
|
||||
func (h *ResourceHandler) GetVariants(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
thumbnails, err := h.files.GetThumbnailsByFileID(id)
|
||||
variants, err := h.resources.GetVariantsByResourceID(id)
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to fetch thumbnails")
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to fetch variants")
|
||||
return
|
||||
}
|
||||
|
||||
type thumbnailResponse struct {
|
||||
ID string `json:"id"`
|
||||
PageNumber int `json:"pageNumber"`
|
||||
ResolutionLabel string `json:"resolutionLabel"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
URL string `json:"url"`
|
||||
MimeType string `json:"mimeType"`
|
||||
type variantResponse struct {
|
||||
ID string `json:"id"`
|
||||
PageNumber int `json:"pageNumber"`
|
||||
VariantType string `json:"variantType"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
URL string `json:"url"`
|
||||
MimeType string `json:"mimeType"`
|
||||
}
|
||||
|
||||
resp := make([]thumbnailResponse, len(thumbnails))
|
||||
for i, t := range thumbnails {
|
||||
resp[i] = thumbnailResponse{
|
||||
ID: t.ID,
|
||||
PageNumber: t.PageNumber,
|
||||
ResolutionLabel: t.ResolutionLabel,
|
||||
Width: t.Width,
|
||||
Height: t.Height,
|
||||
URL: h.urls.GenerateThumbnailURL(t.ID),
|
||||
MimeType: t.MimeType,
|
||||
resp := make([]variantResponse, len(variants))
|
||||
for i, v := range variants {
|
||||
resp[i] = variantResponse{
|
||||
ID: v.ID,
|
||||
PageNumber: v.PageNumber,
|
||||
VariantType: v.VariantType,
|
||||
Width: v.Width,
|
||||
Height: v.Height,
|
||||
URL: h.urls.GenerateVariantURL(v.ID),
|
||||
MimeType: v.MimeType,
|
||||
}
|
||||
}
|
||||
|
||||
api.Success(c, resp)
|
||||
}
|
||||
|
||||
func (h *FileHandler) ServeThumbnail(c *gin.Context) {
|
||||
func (h *ResourceHandler) ServeVariant(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
exp, _ := strconv.ParseInt(c.Query("expires"), 10, 64)
|
||||
sig := c.Query("sig")
|
||||
@@ -437,16 +436,16 @@ func (h *FileHandler) ServeThumbnail(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
storagePath, err := h.files.GetThumbnailStoragePath(id)
|
||||
storagePath, err := h.resources.GetVariantStoragePath(id)
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusNotFound, "THUMBNAIL_NOT_FOUND", "Thumbnail not found")
|
||||
api.Error(c, http.StatusNotFound, "VARIANT_NOT_FOUND", "Variant not found")
|
||||
return
|
||||
}
|
||||
|
||||
c.File(path.Clean(storagePath))
|
||||
}
|
||||
|
||||
func (h *FileHandler) CheckDuplicates(c *gin.Context) {
|
||||
func (h *ResourceHandler) CheckDuplicates(c *gin.Context) {
|
||||
var body struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Size int64 `json:"size" binding:"required"`
|
||||
@@ -457,7 +456,7 @@ func (h *FileHandler) CheckDuplicates(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
duplicates, err := h.files.FindDuplicatesByNameSize(body.Name, body.Size)
|
||||
duplicates, err := h.resources.FindDuplicatesByNameSize(body.Name, body.Size)
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to check duplicates")
|
||||
return
|
||||
@@ -475,7 +474,7 @@ func (h *FileHandler) CheckDuplicates(c *gin.Context) {
|
||||
resp := make([]dupResponse, len(duplicates))
|
||||
for i, d := range duplicates {
|
||||
resp[i] = dupResponse{
|
||||
ID: d.ID,
|
||||
ID: d.ID.String(),
|
||||
Name: d.Name,
|
||||
MimeType: d.MimeType,
|
||||
Size: d.Size,
|
||||
@@ -14,29 +14,65 @@ func SetupRoutes(r *gin.Engine, h *Handler, authMiddleware *auth.AuthService) {
|
||||
api.POST("/auth/login", h.Auth.Login)
|
||||
api.POST("/auth/refresh", h.Auth.Refresh)
|
||||
api.POST("/auth/logout", h.Auth.Logout)
|
||||
api.GET("/files/download/:id", h.File.Download)
|
||||
api.GET("/thumbnails/:id", h.File.ServeThumbnail)
|
||||
api.GET("/resources/download/:id", h.Resource.Download)
|
||||
api.GET("/variants/:id", h.Resource.ServeVariant)
|
||||
|
||||
// Protected
|
||||
protected := api.Group("")
|
||||
protected.Use(authMiddleware.RequireAuth())
|
||||
|
||||
protected.GET("/files", h.File.List)
|
||||
protected.POST("/files/upload", h.File.Upload)
|
||||
protected.POST("/files/move", h.File.MoveFiles)
|
||||
protected.POST("/files/folders", h.File.CreateFolder)
|
||||
protected.GET("/files/folders", h.File.ListFolders)
|
||||
protected.GET("/files/folders/:id/files", h.File.ListFilesByParent)
|
||||
protected.DELETE("/files/:id", h.File.Delete)
|
||||
protected.GET("/files/:id", h.File.Get)
|
||||
// Resources (replaces /files)
|
||||
protected.GET("/resources", h.Resource.List)
|
||||
protected.POST("/resources/upload", h.Resource.Upload)
|
||||
protected.POST("/resources/move", h.Resource.MoveResources)
|
||||
protected.POST("/resources/folders", h.Resource.CreateFolder)
|
||||
protected.GET("/resources/folders", h.Resource.ListFolders)
|
||||
protected.GET("/resources/folders/:id/resources", h.Resource.ListByParent)
|
||||
protected.DELETE("/resources/:id", h.Resource.Delete)
|
||||
protected.GET("/resources/:id", h.Resource.Get)
|
||||
|
||||
protected.POST("/files/:id/tags", h.File.AddTags)
|
||||
protected.GET("/files/:id/tags", h.File.GetTags)
|
||||
// Tags on resources
|
||||
protected.POST("/resources/:id/tags", h.Resource.AddTags)
|
||||
protected.GET("/resources/:id/tags", h.Resource.GetTags)
|
||||
|
||||
// Variants
|
||||
protected.GET("/resources/:id/variants", h.Resource.GetVariants)
|
||||
|
||||
// Dedup check
|
||||
protected.POST("/resources/dedup-check", h.Resource.CheckDuplicates)
|
||||
|
||||
// Sharing (ReBAC)
|
||||
protected.POST("/resources/:id/share", h.Share.Grant)
|
||||
protected.DELETE("/resources/:id/share/:userId", h.Share.Revoke)
|
||||
protected.GET("/resources/:id/share", h.Share.List)
|
||||
protected.GET("/resources/:id/access", h.Share.Check)
|
||||
|
||||
// Device management
|
||||
protected.GET("/devices", h.Device.List)
|
||||
protected.POST("/devices", h.Device.Register)
|
||||
|
||||
// Placements
|
||||
protected.GET("/resources/:id/placements", h.Resource.GetVariants)
|
||||
|
||||
// Sync
|
||||
protected.POST("/sync/pull", h.Sync.Pull)
|
||||
protected.POST("/sync/push", h.Sync.Push)
|
||||
|
||||
// OCR
|
||||
protected.POST("/ocr/jobs", h.OCR.CreateJob)
|
||||
protected.GET("/ocr/jobs/:id", h.OCR.GetJobStatus)
|
||||
|
||||
protected.GET("/files/:id/thumbnails", h.File.GetThumbnails)
|
||||
|
||||
protected.POST("/files/dedup-check", h.File.CheckDuplicates)
|
||||
// Legacy /files/* endpoints (maintain backward compatibility)
|
||||
protected.GET("/files", h.Resource.List)
|
||||
protected.POST("/files/upload", h.Resource.Upload)
|
||||
protected.POST("/files/move", h.Resource.MoveResources)
|
||||
protected.POST("/files/folders", h.Resource.CreateFolder)
|
||||
protected.GET("/files/folders", h.Resource.ListFolders)
|
||||
protected.GET("/files/folders/:id/files", h.Resource.ListByParent)
|
||||
protected.DELETE("/files/:id", h.Resource.Delete)
|
||||
protected.GET("/files/:id", h.Resource.Get)
|
||||
protected.POST("/files/:id/tags", h.Resource.AddTags)
|
||||
protected.GET("/files/:id/tags", h.Resource.GetTags)
|
||||
protected.GET("/files/:id/thumbnails", h.Resource.GetVariants)
|
||||
protected.POST("/files/dedup-check", h.Resource.CheckDuplicates)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/vaultdrop/backend/internal/auth"
|
||||
"github.com/vaultdrop/backend/internal/service"
|
||||
"github.com/vaultdrop/backend/pkg/api"
|
||||
)
|
||||
|
||||
type ShareHandler struct {
|
||||
rebac *service.RebacService
|
||||
}
|
||||
|
||||
func (h *ShareHandler) Grant(c *gin.Context) {
|
||||
userID := c.GetString(auth.UserIDKey)
|
||||
resourceID := c.Param("id")
|
||||
|
||||
var body struct {
|
||||
SubjectUserID string `json:"subject_user_id" binding:"required"`
|
||||
Role string `json:"role" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
api.Error(c, http.StatusBadRequest, "INVALID_BODY", "subject_user_id and role are required")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.rebac.GrantRole(userID, resourceID, body.SubjectUserID, body.Role); err != nil {
|
||||
api.Error(c, http.StatusForbidden, "FORBIDDEN", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
api.Success(c, gin.H{"granted": true})
|
||||
}
|
||||
|
||||
func (h *ShareHandler) Revoke(c *gin.Context) {
|
||||
userID := c.GetString(auth.UserIDKey)
|
||||
resourceID := c.Param("id")
|
||||
subjectID := c.Param("userId")
|
||||
|
||||
if err := h.rebac.RevokeRole(userID, resourceID, subjectID); err != nil {
|
||||
api.Error(c, http.StatusForbidden, "FORBIDDEN", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
api.Success(c, gin.H{"revoked": true})
|
||||
}
|
||||
|
||||
func (h *ShareHandler) List(c *gin.Context) {
|
||||
resourceID := c.Param("id")
|
||||
|
||||
relations, err := h.rebac.ListShares(resourceID)
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to list shares")
|
||||
return
|
||||
}
|
||||
|
||||
type shareResponse struct {
|
||||
UserID string `json:"user_id"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
resp := make([]shareResponse, len(relations))
|
||||
for i, r := range relations {
|
||||
resp[i] = shareResponse{
|
||||
UserID: r.SubjectUserID.String(),
|
||||
Role: r.Role,
|
||||
}
|
||||
}
|
||||
|
||||
api.Success(c, resp)
|
||||
}
|
||||
|
||||
func (h *ShareHandler) Check(c *gin.Context) {
|
||||
userID := c.GetString(auth.UserIDKey)
|
||||
resourceID := c.Param("id")
|
||||
|
||||
role, err := h.rebac.ResolveEffectiveRole(userID, resourceID)
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to resolve role")
|
||||
return
|
||||
}
|
||||
|
||||
api.Success(c, gin.H{
|
||||
"role": role,
|
||||
"access": role != "",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/vaultdrop/backend/internal/service"
|
||||
"github.com/vaultdrop/backend/pkg/api"
|
||||
)
|
||||
|
||||
type SyncHandler struct {
|
||||
sync *service.SyncService
|
||||
}
|
||||
|
||||
func (h *SyncHandler) Pull(c *gin.Context) {
|
||||
var body struct {
|
||||
LocationID string `json:"location_id"`
|
||||
}
|
||||
c.ShouldBindJSON(&body)
|
||||
|
||||
var err error
|
||||
var items interface{}
|
||||
if body.LocationID != "" {
|
||||
items, err = h.sync.ListPending(body.LocationID)
|
||||
} else {
|
||||
items, err = h.sync.ListAllPending()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to list pending sync items")
|
||||
return
|
||||
}
|
||||
|
||||
api.Success(c, items)
|
||||
}
|
||||
|
||||
func (h *SyncHandler) Push(c *gin.Context) {
|
||||
var body struct {
|
||||
LocationID string `json:"location_id" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
api.Error(c, http.StatusBadRequest, "INVALID_BODY", "location_id is required")
|
||||
return
|
||||
}
|
||||
|
||||
items, err := h.sync.ListPending(body.LocationID)
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, "DB_ERROR", "Failed to list pending sync items")
|
||||
return
|
||||
}
|
||||
|
||||
api.Created(c, gin.H{
|
||||
"pending": len(items),
|
||||
"message": "Push initiated",
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user