refactor: reduce cyclomatic complexity in filesConnector.Connect

Split the Connect handler into smaller focused functions:
- handleRequest: main request processing
- createDualReadWriter: setup dependencies
- parseRequestOptions: extract request options
- handleDirectoryListing: GET directory requests
- handleMethodRequest: route to method handlers
- handleGet/handlePost/handlePut/handleDelete: method-specific logic
- handleMove: move operation logic
This commit is contained in:
Roberto Jimenez Sanchez
2025-12-17 20:44:58 +01:00
parent fc5ad29b68
commit 10825c242d
+210 -163
View File
@@ -75,172 +75,219 @@ func (c *filesConnector) Connect(ctx context.Context, name string, opts runtime.
ctx = logging.Context(ctx, logger)
return WithTimeout(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
repo, err := c.getRepo(ctx, r.Method, name)
if err != nil {
logger.Debug("failed to find repository", "error", err)
responder.Error(err)
return
}
readWriter, ok := repo.(repository.ReaderWriter)
if !ok {
responder.Error(apierrors.NewBadRequest("repository does not support read-writing"))
return
}
parser, err := c.parsers.GetParser(ctx, readWriter)
if err != nil {
responder.Error(fmt.Errorf("failed to get parser: %w", err))
return
}
clients, err := c.clients.Clients(ctx, repo.Config().Namespace)
if err != nil {
responder.Error(fmt.Errorf("failed to get clients: %w", err))
return
}
folderClient, err := clients.Folder(ctx)
if err != nil {
responder.Error(fmt.Errorf("failed to get folder client: %w", err))
return
}
folders := resources.NewFolderManager(readWriter, folderClient, resources.NewEmptyFolderTree())
dualReadWriter := resources.NewDualReadWriter(readWriter, parser, folders, c.access)
query := r.URL.Query()
opts := resources.DualWriteOptions{
Ref: query.Get("ref"),
Message: query.Get("message"),
SkipDryRun: query.Get("skipDryRun") == "true",
OriginalPath: query.Get("originalPath"),
Branch: repo.Config().Branch(),
}
logger := logger.With("url", r.URL.Path, "ref", opts.Ref, "message", opts.Message)
ctx := logging.Context(r.Context(), logger)
opts.Path, err = pathAfterPrefix(r.URL.Path, fmt.Sprintf("/%s/files", name))
if err != nil {
responder.Error(apierrors.NewBadRequest(err.Error()))
return
}
if err := resources.IsPathSupported(opts.Path); err != nil {
responder.Error(apierrors.NewBadRequest(err.Error()))
return
}
isDir := safepath.IsDir(opts.Path)
if r.Method == http.MethodGet && isDir {
// Directory listing requires repositories:read permission
if err := c.authorizeListFiles(ctx, name); err != nil {
responder.Error(err)
return
}
files, err := c.listFolderFiles(ctx, opts.Path, opts.Ref, readWriter)
if err != nil {
responder.Error(err)
return
}
responder.Object(http.StatusOK, files)
return
}
if opts.Path == "" {
responder.Error(apierrors.NewBadRequest("missing request path"))
return
}
var obj *provisioning.ResourceWrapper
code := http.StatusOK
switch r.Method {
case http.MethodGet:
resource, err := dualReadWriter.Read(ctx, opts.Path, opts.Ref)
if err != nil {
respondWithError(responder, err)
return
}
obj = resource.AsResourceWrapper()
case http.MethodPost:
// Check if this is a move operation first (originalPath query parameter is present)
if opts.OriginalPath != "" {
// For move operations, only read body for file moves (not directory moves)
if !isDir {
opts.Data, err = readBody(r, filesMaxBodySize)
if err != nil {
responder.Error(err)
return
}
}
resource, err := dualReadWriter.MoveResource(ctx, opts)
if err != nil {
respondWithError(responder, err)
return
}
obj = resource.AsResourceWrapper()
} else if isDir {
obj, err = dualReadWriter.CreateFolder(ctx, opts)
} else {
opts.Data, err = readBody(r, filesMaxBodySize)
if err != nil {
responder.Error(err)
return
}
var resource *resources.ParsedResource
resource, err = dualReadWriter.CreateResource(ctx, opts)
if err != nil {
respondWithError(responder, err)
return
}
obj = resource.AsResourceWrapper()
}
case http.MethodPut:
// TODO: document in API specification
if isDir {
err = apierrors.NewMethodNotSupported(provisioning.RepositoryResourceInfo.GroupResource(), r.Method)
} else {
opts.Data, err = readBody(r, filesMaxBodySize)
if err != nil {
responder.Error(err)
return
}
resource, err := dualReadWriter.UpdateResource(ctx, opts)
if err != nil {
respondWithError(responder, err)
return
}
obj = resource.AsResourceWrapper()
}
case http.MethodDelete:
resource, err := dualReadWriter.Delete(ctx, opts)
if err != nil {
respondWithError(responder, err)
return
}
obj = resource.AsResourceWrapper()
default:
err = apierrors.NewMethodNotSupported(provisioning.RepositoryResourceInfo.GroupResource(), r.Method)
}
if err != nil {
logger.Debug("got an error after processing request", "error", err)
responder.Error(err)
return
}
if len(obj.Errors) > 0 {
code = http.StatusPartialContent
}
logger.Debug("request resulted in valid object", "object", obj)
responder.Object(code, obj)
c.handleRequest(ctx, name, r, responder, logger)
}), 30*time.Second), nil
}
// handleRequest processes the HTTP request for files operations.
func (c *filesConnector) handleRequest(ctx context.Context, name string, r *http.Request, responder rest.Responder, logger logging.Logger) {
repo, err := c.getRepo(ctx, r.Method, name)
if err != nil {
logger.Debug("failed to find repository", "error", err)
responder.Error(err)
return
}
readWriter, ok := repo.(repository.ReaderWriter)
if !ok {
responder.Error(apierrors.NewBadRequest("repository does not support read-writing"))
return
}
dualReadWriter, err := c.createDualReadWriter(ctx, repo, readWriter)
if err != nil {
responder.Error(err)
return
}
opts, err := c.parseRequestOptions(r, name, repo)
if err != nil {
responder.Error(apierrors.NewBadRequest(err.Error()))
return
}
logger = logger.With("url", r.URL.Path, "ref", opts.Ref, "message", opts.Message)
ctx = logging.Context(r.Context(), logger)
// Handle directory listing separately
isDir := safepath.IsDir(opts.Path)
if r.Method == http.MethodGet && isDir {
c.handleDirectoryListing(ctx, name, opts, readWriter, responder)
return
}
if opts.Path == "" {
responder.Error(apierrors.NewBadRequest("missing request path"))
return
}
obj, err := c.handleMethodRequest(ctx, r, opts, isDir, dualReadWriter)
if err != nil {
logger.Debug("got an error after processing request", "error", err)
respondWithError(responder, err)
return
}
code := http.StatusOK
if len(obj.Errors) > 0 {
code = http.StatusPartialContent
}
logger.Debug("request resulted in valid object", "object", obj)
responder.Object(code, obj)
}
// createDualReadWriter sets up the dual read writer with all required dependencies.
func (c *filesConnector) createDualReadWriter(ctx context.Context, repo repository.Repository, readWriter repository.ReaderWriter) (*resources.DualReadWriter, error) {
parser, err := c.parsers.GetParser(ctx, readWriter)
if err != nil {
return nil, fmt.Errorf("failed to get parser: %w", err)
}
clients, err := c.clients.Clients(ctx, repo.Config().Namespace)
if err != nil {
return nil, fmt.Errorf("failed to get clients: %w", err)
}
folderClient, err := clients.Folder(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get folder client: %w", err)
}
folders := resources.NewFolderManager(readWriter, folderClient, resources.NewEmptyFolderTree())
return resources.NewDualReadWriter(readWriter, parser, folders, c.access), nil
}
// parseRequestOptions extracts options from the HTTP request.
func (c *filesConnector) parseRequestOptions(r *http.Request, name string, repo repository.Repository) (resources.DualWriteOptions, error) {
query := r.URL.Query()
opts := resources.DualWriteOptions{
Ref: query.Get("ref"),
Message: query.Get("message"),
SkipDryRun: query.Get("skipDryRun") == "true",
OriginalPath: query.Get("originalPath"),
Branch: repo.Config().Branch(),
}
path, err := pathAfterPrefix(r.URL.Path, fmt.Sprintf("/%s/files", name))
if err != nil {
return opts, err
}
opts.Path = path
if err := resources.IsPathSupported(opts.Path); err != nil {
return opts, err
}
return opts, nil
}
// handleDirectoryListing handles GET requests for directory listing.
func (c *filesConnector) handleDirectoryListing(ctx context.Context, name string, opts resources.DualWriteOptions, readWriter repository.ReaderWriter, responder rest.Responder) {
if err := c.authorizeListFiles(ctx, name); err != nil {
responder.Error(err)
return
}
files, err := c.listFolderFiles(ctx, opts.Path, opts.Ref, readWriter)
if err != nil {
responder.Error(err)
return
}
responder.Object(http.StatusOK, files)
}
// handleMethodRequest routes the request to the appropriate handler based on HTTP method.
func (c *filesConnector) handleMethodRequest(ctx context.Context, r *http.Request, opts resources.DualWriteOptions, isDir bool, dualReadWriter *resources.DualReadWriter) (*provisioning.ResourceWrapper, error) {
switch r.Method {
case http.MethodGet:
return c.handleGet(ctx, opts, dualReadWriter)
case http.MethodPost:
return c.handlePost(ctx, r, opts, isDir, dualReadWriter)
case http.MethodPut:
return c.handlePut(ctx, r, opts, isDir, dualReadWriter)
case http.MethodDelete:
return c.handleDelete(ctx, opts, dualReadWriter)
default:
return nil, apierrors.NewMethodNotSupported(provisioning.RepositoryResourceInfo.GroupResource(), r.Method)
}
}
func (c *filesConnector) handleGet(ctx context.Context, opts resources.DualWriteOptions, dualReadWriter *resources.DualReadWriter) (*provisioning.ResourceWrapper, error) {
resource, err := dualReadWriter.Read(ctx, opts.Path, opts.Ref)
if err != nil {
return nil, err
}
return resource.AsResourceWrapper(), nil
}
func (c *filesConnector) handlePost(ctx context.Context, r *http.Request, opts resources.DualWriteOptions, isDir bool, dualReadWriter *resources.DualReadWriter) (*provisioning.ResourceWrapper, error) {
// Check if this is a move operation (originalPath query parameter is present)
if opts.OriginalPath != "" {
return c.handleMove(ctx, r, opts, isDir, dualReadWriter)
}
if isDir {
return dualReadWriter.CreateFolder(ctx, opts)
}
data, err := readBody(r, filesMaxBodySize)
if err != nil {
return nil, err
}
opts.Data = data
resource, err := dualReadWriter.CreateResource(ctx, opts)
if err != nil {
return nil, err
}
return resource.AsResourceWrapper(), nil
}
func (c *filesConnector) handleMove(ctx context.Context, r *http.Request, opts resources.DualWriteOptions, isDir bool, dualReadWriter *resources.DualReadWriter) (*provisioning.ResourceWrapper, error) {
// For move operations, only read body for file moves (not directory moves)
if !isDir {
data, err := readBody(r, filesMaxBodySize)
if err != nil {
return nil, err
}
opts.Data = data
}
resource, err := dualReadWriter.MoveResource(ctx, opts)
if err != nil {
return nil, err
}
return resource.AsResourceWrapper(), nil
}
func (c *filesConnector) handlePut(ctx context.Context, r *http.Request, opts resources.DualWriteOptions, isDir bool, dualReadWriter *resources.DualReadWriter) (*provisioning.ResourceWrapper, error) {
if isDir {
return nil, apierrors.NewMethodNotSupported(provisioning.RepositoryResourceInfo.GroupResource(), r.Method)
}
data, err := readBody(r, filesMaxBodySize)
if err != nil {
return nil, err
}
opts.Data = data
resource, err := dualReadWriter.UpdateResource(ctx, opts)
if err != nil {
return nil, err
}
return resource.AsResourceWrapper(), nil
}
func (c *filesConnector) handleDelete(ctx context.Context, opts resources.DualWriteOptions, dualReadWriter *resources.DualReadWriter) (*provisioning.ResourceWrapper, error) {
resource, err := dualReadWriter.Delete(ctx, opts)
if err != nil {
return nil, err
}
return resource.AsResourceWrapper(), nil
}
// authorizeListFiles checks if the user has repositories:read permission for listing files.
// The access checker handles AccessPolicy identities, namespace resolution, and role-based fallback internally.
func (c *filesConnector) authorizeListFiles(ctx context.Context, repoName string) error {