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.
This commit is contained in:
20
modules/structs/repo_hidden_folder.go
Normal file
20
modules/structs/repo_hidden_folder.go
Normal file
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
|
||||
126
routers/api/v2/hidden_folders.go
Normal file
126
routers/api/v2/hidden_folders.go
Normal file
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user