Changeset 0.22.4 (#146)
This commit is contained in:
@@ -457,6 +457,121 @@ func sanitizeFilename(name string) string {
|
||||
return name
|
||||
}
|
||||
|
||||
// ── Project Files (v0.22.4) ────────────────
|
||||
|
||||
// UploadToProject handles project-scoped file uploads.
|
||||
// POST /api/v1/projects/:id/files
|
||||
func (h *AttachmentHandler) UploadToProject(c *gin.Context) {
|
||||
if h.objStore == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "file storage not configured"})
|
||||
return
|
||||
}
|
||||
|
||||
userID := getUserID(c)
|
||||
projectID := c.Param("id")
|
||||
|
||||
// Verify project access (user owns or is team member)
|
||||
var exists bool
|
||||
err := database.DB.QueryRowContext(c.Request.Context(), database.Q(`
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM projects
|
||||
WHERE id = $1 AND (owner_id = $2 OR team_id IN (
|
||||
SELECT team_id FROM team_members WHERE user_id = $2 AND is_active = true
|
||||
))
|
||||
)
|
||||
`), projectID, userID).Scan(&exists)
|
||||
if err != nil || !exists {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "project not found or access denied"})
|
||||
return
|
||||
}
|
||||
|
||||
file, header, err := c.Request.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no file uploaded"})
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if header.Size > int64(defaultMaxFileSize) {
|
||||
c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "file exceeds size limit"})
|
||||
return
|
||||
}
|
||||
|
||||
// Detect content type
|
||||
buf := make([]byte, 512)
|
||||
n, _ := file.Read(buf)
|
||||
contentType := http.DetectContentType(buf[:n])
|
||||
file.Seek(0, io.SeekStart)
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(header.Filename))
|
||||
if better, ok := extToMIME[ext]; ok && contentType == "application/octet-stream" {
|
||||
contentType = better
|
||||
}
|
||||
|
||||
// Store file
|
||||
storageKey := fmt.Sprintf("projects/%s/%s/%s", projectID, store.NewID()[:8], sanitizeFilename(header.Filename))
|
||||
if err := h.objStore.Put(c.Request.Context(), storageKey, file, header.Size, contentType); err != nil {
|
||||
log.Printf("error: project file upload: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to store file"})
|
||||
return
|
||||
}
|
||||
|
||||
att := models.Attachment{
|
||||
ChannelID: "", // no channel association
|
||||
UserID: userID,
|
||||
ProjectID: &projectID,
|
||||
Filename: header.Filename,
|
||||
ContentType: contentType,
|
||||
SizeBytes: header.Size,
|
||||
StorageKey: storageKey,
|
||||
}
|
||||
|
||||
if err := h.stores.Attachments.Create(c.Request.Context(), &att); err != nil {
|
||||
log.Printf("error: project file create: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save file metadata"})
|
||||
return
|
||||
}
|
||||
|
||||
if h.extQueue != nil {
|
||||
if err := h.extQueue.Enqueue(att.ID, att.StorageKey, contentType, att.Filename); err != nil {
|
||||
log.Printf("warning: project file extraction enqueue failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, att)
|
||||
}
|
||||
|
||||
// ListByProject returns all files uploaded to a project.
|
||||
// GET /api/v1/projects/:id/files
|
||||
func (h *AttachmentHandler) ListByProject(c *gin.Context) {
|
||||
userID := getUserID(c)
|
||||
projectID := c.Param("id")
|
||||
|
||||
// Verify project access
|
||||
var exists bool
|
||||
err := database.DB.QueryRowContext(c.Request.Context(), database.Q(`
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM projects
|
||||
WHERE id = $1 AND (owner_id = $2 OR team_id IN (
|
||||
SELECT team_id FROM team_members WHERE user_id = $2 AND is_active = true
|
||||
))
|
||||
)
|
||||
`), projectID, userID).Scan(&exists)
|
||||
if err != nil || !exists {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "project not found or access denied"})
|
||||
return
|
||||
}
|
||||
|
||||
files, err := h.stores.Attachments.GetByProject(c.Request.Context(), projectID)
|
||||
if err != nil {
|
||||
log.Printf("error: list project files: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list files"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"files": files, "count": len(files)})
|
||||
}
|
||||
|
||||
// extToMIME maps file extensions to MIME types for cases where
|
||||
// http.DetectContentType returns application/octet-stream.
|
||||
var extToMIME = map[string]string{
|
||||
|
||||
Reference in New Issue
Block a user