feat: support office document nodes
This commit is contained in:
@@ -72,6 +72,8 @@ func main() {
|
|||||||
r.Get("/nodes", nodeH.List)
|
r.Get("/nodes", nodeH.List)
|
||||||
r.Post("/folders", nodeH.CreateFolder)
|
r.Post("/folders", nodeH.CreateFolder)
|
||||||
r.Post("/files", nodeH.UploadFile)
|
r.Post("/files", nodeH.UploadFile)
|
||||||
|
r.Get("/office-documents/links", nodeH.ListOfficeLinks)
|
||||||
|
r.Post("/office-documents", nodeH.CreateOfficeDocument)
|
||||||
r.Route("/nodes/{id}", func(r chi.Router) {
|
r.Route("/nodes/{id}", func(r chi.Router) {
|
||||||
r.Get("/", nodeH.Get)
|
r.Get("/", nodeH.Get)
|
||||||
r.Patch("/", nodeH.Update)
|
r.Patch("/", nodeH.Update)
|
||||||
|
|||||||
@@ -73,6 +73,63 @@ func (h *NodeHandler) CreateFolder(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeJSON(w, http.StatusCreated, node)
|
writeJSON(w, http.StatusCreated, node)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *NodeHandler) ListOfficeLinks(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userID := commonmw.GetUserID(r.Context())
|
||||||
|
links, err := h.repo.ListOfficeExternalURLs(r.Context(), userID, subordinates(r))
|
||||||
|
if err != nil {
|
||||||
|
writeInternalError(w, r, err, "failed to list office links")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, links)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *NodeHandler) CreateOfficeDocument(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req model.CreateOfficeDocumentRequest
|
||||||
|
if err := decodeJSON(r, &req); err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, "invalid json")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.Title = strings.TrimSpace(req.Title)
|
||||||
|
req.OfficeID = strings.TrimSpace(req.OfficeID)
|
||||||
|
req.OfficeFormat = strings.Trim(strings.ToLower(req.OfficeFormat), ". ")
|
||||||
|
if req.Title == "" {
|
||||||
|
writeError(w, http.StatusBadRequest, "title is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.OfficeID == "" {
|
||||||
|
writeError(w, http.StatusBadRequest, "office_id is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !allowedOfficeFormat(req.OfficeFormat) {
|
||||||
|
writeError(w, http.StatusBadRequest, "office_format is not allowed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userID := commonmw.GetUserID(r.Context())
|
||||||
|
if !h.requireWritableParent(w, r, userID, req.ParentID) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
externalURL := "/office/" + req.OfficeID
|
||||||
|
originalFilename := req.Title + "." + req.OfficeFormat
|
||||||
|
mimeType := req.OfficeFormat
|
||||||
|
node, err := h.repo.CreateOfficeDocument(r.Context(), &model.Node{
|
||||||
|
ParentID: req.ParentID,
|
||||||
|
Title: req.Title,
|
||||||
|
OwnerUserID: userID,
|
||||||
|
OriginalFilename: &originalFilename,
|
||||||
|
MimeType: &mimeType,
|
||||||
|
Extension: &req.OfficeFormat,
|
||||||
|
SizeBytes: req.SizeBytes,
|
||||||
|
OfficeFormat: &req.OfficeFormat,
|
||||||
|
ExternalURL: &externalURL,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
writeInternalError(w, r, err, "failed to create office document")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.repo.Audit(r.Context(), userID, "files.office_document_create", "files_node", node.ID, "{}")
|
||||||
|
writeJSON(w, http.StatusCreated, node)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *NodeHandler) UploadFile(w http.ResponseWriter, r *http.Request) {
|
func (h *NodeHandler) UploadFile(w http.ResponseWriter, r *http.Request) {
|
||||||
if !h.store.Configured() {
|
if !h.store.Configured() {
|
||||||
writeError(w, http.StatusServiceUnavailable, "storage not configured")
|
writeError(w, http.StatusServiceUnavailable, "storage not configured")
|
||||||
@@ -376,6 +433,15 @@ func emptyToNil(v string) *string {
|
|||||||
return &v
|
return &v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func allowedOfficeFormat(format string) bool {
|
||||||
|
switch format {
|
||||||
|
case "doc", "docx", "odt", "xls", "xlsx", "xlsm", "ods", "ppt", "pptx", "odp":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func subordinates(r *http.Request) []string {
|
func subordinates(r *http.Request) []string {
|
||||||
ids := csvHeader(r, "X-User-Subordinates")
|
ids := csvHeader(r, "X-User-Subordinates")
|
||||||
if len(ids) == 0 {
|
if len(ids) == 0 {
|
||||||
|
|||||||
@@ -54,6 +54,14 @@ type CreateFolderRequest struct {
|
|||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CreateOfficeDocumentRequest struct {
|
||||||
|
ParentID *string `json:"parent_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
OfficeID string `json:"office_id"`
|
||||||
|
OfficeFormat string `json:"office_format"`
|
||||||
|
SizeBytes int64 `json:"size_bytes"`
|
||||||
|
}
|
||||||
|
|
||||||
type UpdateNodeRequest struct {
|
type UpdateNodeRequest struct {
|
||||||
ParentID *string `json:"parent_id"`
|
ParentID *string `json:"parent_id"`
|
||||||
Title *string `json:"title"`
|
Title *string `json:"title"`
|
||||||
|
|||||||
@@ -127,6 +127,49 @@ func (r *NodeRepository) CreateFile(ctx context.Context, n *model.Node) (*model.
|
|||||||
`, n.ParentID, n.Title, n.OwnerUserID, n.StorageKey, n.OriginalFilename, n.MimeType, n.Extension, n.SizeBytes).Scan)
|
`, n.ParentID, n.Title, n.OwnerUserID, n.StorageKey, n.OriginalFilename, n.MimeType, n.Extension, n.SizeBytes).Scan)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *NodeRepository) CreateOfficeDocument(ctx context.Context, n *model.Node) (*model.Node, error) {
|
||||||
|
return scanNode(r.pool.QueryRow(ctx, `
|
||||||
|
INSERT INTO files_nodes
|
||||||
|
(parent_id, node_type, title, owner_user_id, created_by, original_filename,
|
||||||
|
mime_type, extension, size_bytes, office_format, external_url)
|
||||||
|
VALUES ($1, 'office_document', $2, $3, $3, $4, $5, $6, $7, $8, $9)
|
||||||
|
RETURNING id, parent_id, node_type, title, owner_user_id, owner_department_id,
|
||||||
|
created_by, updated_by, storage_key, original_filename, mime_type,
|
||||||
|
extension, size_bytes, office_format, external_url, version,
|
||||||
|
'edit' AS effective_access, created_at, updated_at, trashed_at, purge_after, deleted_at
|
||||||
|
`, n.ParentID, n.Title, n.OwnerUserID, n.OriginalFilename, n.MimeType, n.Extension, n.SizeBytes, n.OfficeFormat, n.ExternalURL).Scan)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NodeRepository) ListOfficeExternalURLs(ctx context.Context, userID string, subordinateIDs []string) ([]string, error) {
|
||||||
|
rows, err := r.pool.Query(ctx, `
|
||||||
|
SELECT DISTINCT n.external_url
|
||||||
|
FROM files_nodes n
|
||||||
|
WHERE n.deleted_at IS NULL
|
||||||
|
AND n.node_type = 'office_document'
|
||||||
|
AND n.external_url IS NOT NULL
|
||||||
|
AND (
|
||||||
|
n.owner_user_id = $1
|
||||||
|
OR has_node_access(n.id, $1)
|
||||||
|
OR n.owner_user_id::text = ANY($2::text[])
|
||||||
|
)
|
||||||
|
ORDER BY n.external_url
|
||||||
|
`, userID, subordinateIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
out := make([]string, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var url string
|
||||||
|
if err := rows.Scan(&url); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, url)
|
||||||
|
}
|
||||||
|
return out, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
func (r *NodeRepository) Update(ctx context.Context, id, actorID string, req model.UpdateNodeRequest) (*model.Node, error) {
|
func (r *NodeRepository) Update(ctx context.Context, id, actorID string, req model.UpdateNodeRequest) (*model.Node, error) {
|
||||||
return scanNode(r.pool.QueryRow(ctx, `
|
return scanNode(r.pool.QueryRow(ctx, `
|
||||||
UPDATE files_nodes
|
UPDATE files_nodes
|
||||||
|
|||||||
7
migrations/004_office_document_nodes.down.sql
Normal file
7
migrations/004_office_document_nodes.down.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
ALTER TABLE files_nodes
|
||||||
|
DROP CONSTRAINT IF EXISTS files_nodes_file_storage_check;
|
||||||
|
|
||||||
|
ALTER TABLE files_nodes
|
||||||
|
ADD CONSTRAINT files_nodes_file_storage_check CHECK (
|
||||||
|
node_type IN ('folder', 'google_sheet') OR storage_key IS NOT NULL
|
||||||
|
);
|
||||||
7
migrations/004_office_document_nodes.up.sql
Normal file
7
migrations/004_office_document_nodes.up.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
ALTER TABLE files_nodes
|
||||||
|
DROP CONSTRAINT IF EXISTS files_nodes_file_storage_check;
|
||||||
|
|
||||||
|
ALTER TABLE files_nodes
|
||||||
|
ADD CONSTRAINT files_nodes_file_storage_check CHECK (
|
||||||
|
node_type IN ('folder', 'google_sheet', 'office_document') OR storage_key IS NOT NULL
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user