From 7844b8d2a4a885020b13bfca39cdf866f4669f7b Mon Sep 17 00:00:00 2001 From: logikonline Date: Tue, 27 Jan 2026 14:02:48 -0500 Subject: [PATCH] feat(api): add hidden folders management endpoints to v2 API Implements three new endpoints for managing repository hidden folders: - GET /repos/{owner}/{repo}/hidden-folders - list all hidden folders - PUT /repos/{owner}/{repo}/hidden-folders - add a hidden folder - DELETE /repos/{owner}/{repo}/hidden-folders - remove a hidden folder All write operations require repository admin permissions. --- modules/structs/repo_hidden_folder.go | 20 ++++ routers/api/v2/api.go | 9 ++ routers/api/v2/hidden_folders.go | 126 ++++++++++++++++++++++++++ 3 files changed, 155 insertions(+) create mode 100644 modules/structs/repo_hidden_folder.go create mode 100644 routers/api/v2/hidden_folders.go diff --git a/modules/structs/repo_hidden_folder.go b/modules/structs/repo_hidden_folder.go new file mode 100644 index 0000000000..89c53231a8 --- /dev/null +++ b/modules/structs/repo_hidden_folder.go @@ -0,0 +1,20 @@ +// Copyright 2026 MarketAlly. All rights reserved. +// SPDX-License-Identifier: MIT + +package structs + +// HiddenFolderV2 represents a hidden folder in a repository. +type HiddenFolderV2 struct { + Path string `json:"path"` + CreatedAt int64 `json:"created_at"` +} + +// HiddenFolderListV2 is the response for listing hidden folders. +type HiddenFolderListV2 struct { + Folders []*HiddenFolderV2 `json:"folders"` +} + +// HiddenFolderOptionV2 is the request body for adding or removing a hidden folder. +type HiddenFolderOptionV2 struct { + Path string `json:"path" binding:"Required"` +} diff --git a/routers/api/v2/api.go b/routers/api/v2/api.go index c008432419..f2f1e60dff 100644 --- a/routers/api/v2/api.go +++ b/routers/api/v2/api.go @@ -174,6 +174,15 @@ func Routes() *web.Router { m.Get("/config", repoAssignmentWithPublicAccess(), GetPagesConfig) m.Get("/content", repoAssignmentWithPublicAccess(), GetPagesContent) }) + + // Hidden folders API - manage hidden folders for a repository + m.Group("/repos/{owner}/{repo}/hidden-folders", func() { + m.Get("", repoAssignment(), ListHiddenFoldersV2) + m.Group("", func() { + m.Put("", web.Bind(api.HiddenFolderOptionV2{}), AddHiddenFolderV2) + m.Delete("", web.Bind(api.HiddenFolderOptionV2{}), RemoveHiddenFolderV2) + }, repoAssignment(), reqToken()) + }) }) return m diff --git a/routers/api/v2/hidden_folders.go b/routers/api/v2/hidden_folders.go new file mode 100644 index 0000000000..8edb4da31d --- /dev/null +++ b/routers/api/v2/hidden_folders.go @@ -0,0 +1,126 @@ +// Copyright 2026 MarketAlly. All rights reserved. +// SPDX-License-Identifier: MIT + +package v2 + +import ( + "net/http" + "strings" + + repo_model "code.gitcaddy.com/server/v3/models/repo" + apierrors "code.gitcaddy.com/server/v3/modules/errors" + api "code.gitcaddy.com/server/v3/modules/structs" + "code.gitcaddy.com/server/v3/modules/web" + "code.gitcaddy.com/server/v3/services/context" +) + +// ListHiddenFoldersV2 returns all hidden folders for a repository. +func ListHiddenFoldersV2(ctx *context.APIContext) { + folders, err := repo_model.GetHiddenFolders(ctx, ctx.Repo.Repository.ID) + if err != nil { + ctx.APIErrorInternal(err) + return + } + + result := make([]*api.HiddenFolderV2, 0, len(folders)) + for _, f := range folders { + result = append(result, &api.HiddenFolderV2{ + Path: f.FolderPath, + CreatedAt: int64(f.CreatedUnix), + }) + } + + ctx.JSON(http.StatusOK, &api.HiddenFolderListV2{Folders: result}) +} + +// AddHiddenFolderV2 hides a folder in a repository. +func AddHiddenFolderV2(ctx *context.APIContext) { + if !ctx.Repo.Permission.IsAdmin() && !ctx.IsUserSiteAdmin() { + ctx.APIErrorWithCode(apierrors.PermRepoAdminRequired) + return + } + + form := web.GetForm(ctx).(*api.HiddenFolderOptionV2) + folderPath := strings.Trim(form.Path, "/") + + if folderPath == "" || strings.Contains(folderPath, "..") { + ctx.APIErrorWithCode(apierrors.ValInvalidInput, map[string]any{ + "field": "path", + "error": "path must not be empty or contain '..'", + }) + return + } + + isHidden, err := repo_model.IsHiddenFolder(ctx, ctx.Repo.Repository.ID, folderPath) + if err != nil { + ctx.APIErrorInternal(err) + return + } + if isHidden { + ctx.APIErrorWithCode(apierrors.ResourceConflict, map[string]any{ + "path": folderPath, + "error": "folder is already hidden", + }) + return + } + + if err := repo_model.AddHiddenFolder(ctx, ctx.Repo.Repository.ID, folderPath); err != nil { + ctx.APIErrorInternal(err) + return + } + + // Fetch the created record to get the timestamp + folders, err := repo_model.GetHiddenFolders(ctx, ctx.Repo.Repository.ID) + if err != nil { + ctx.APIErrorInternal(err) + return + } + for _, f := range folders { + if f.FolderPath == folderPath { + ctx.JSON(http.StatusCreated, &api.HiddenFolderV2{ + Path: f.FolderPath, + CreatedAt: int64(f.CreatedUnix), + }) + return + } + } + + // Fallback if record not found (shouldn't happen) + ctx.JSON(http.StatusCreated, &api.HiddenFolderV2{Path: folderPath}) +} + +// RemoveHiddenFolderV2 unhides a folder in a repository. +func RemoveHiddenFolderV2(ctx *context.APIContext) { + if !ctx.Repo.Permission.IsAdmin() && !ctx.IsUserSiteAdmin() { + ctx.APIErrorWithCode(apierrors.PermRepoAdminRequired) + return + } + + form := web.GetForm(ctx).(*api.HiddenFolderOptionV2) + folderPath := strings.Trim(form.Path, "/") + + if folderPath == "" { + ctx.APIErrorWithCode(apierrors.ValInvalidInput, map[string]any{ + "field": "path", + "error": "path must not be empty", + }) + return + } + + isHidden, err := repo_model.IsHiddenFolder(ctx, ctx.Repo.Repository.ID, folderPath) + if err != nil { + ctx.APIErrorInternal(err) + return + } + if !isHidden { + ctx.APIErrorNotFound("folder is not hidden") + return + } + + if err := repo_model.RemoveHiddenFolder(ctx, ctx.Repo.Repository.ID, folderPath); err != nil { + ctx.APIErrorInternal(err) + return + } + + ctx.Status(http.StatusNoContent) +}