package handlers import ( "fmt" "io" "net/url" "strings" "polar/internal/git" "polar/internal/web/middleware" "github.com/gofiber/fiber/v2" ) // RepoNewFilePage renders the web form for creating a file. // GET /:owner/:repo/new-file func (h *Handler) RepoNewFilePage(c *fiber.Ctx) error { return h.renderFileForm(c, "file") } // RepoNewFolderPage renders the form for creating a folder. Git cannot store // empty directories, so the folder is tracked through a .gitkeep placeholder. // GET /:owner/:repo/new-folder func (h *Handler) RepoNewFolderPage(c *fiber.Ctx) error { return h.renderFileForm(c, "folder") } func (h *Handler) renderFileForm(c *fiber.Ctx, mode string) error { r, diskPath, err := h.loadAuthorizedRepo(c, true) if err != nil { return nil } titleKey := "file.new_file" if mode == "folder" { titleKey = "file.new_folder" } ref := strings.TrimSpace(c.Query("ref")) if ref == "" { ref, _ = git.EnsureDefaultBranch(c.UserContext(), diskPath, r.DefaultBranch) } subPath := strings.Trim(strings.ReplaceAll(c.Query("path"), "\\", "/"), "/") data := h.baseData(c, h.tr(c, titleKey)) data["Repo"] = r data["Ref"] = ref data["SubPath"] = subPath data["Mode"] = mode data["ActiveTab"] = "code" data["CanWrite"] = true data["Branches"], _ = git.ListBranches(c.UserContext(), diskPath) return h.render(c, fiber.StatusOK, "file_new", data) } // RepoCreateFileSubmit commits a new file to the selected branch. // POST /:owner/:repo/new-file func (h *Handler) RepoCreateFileSubmit(c *fiber.Ctx) error { r, diskPath, err := h.loadAuthorizedRepo(c, true) if err != nil { return nil } u := middleware.CurrentUser(c) ref := strings.TrimSpace(c.FormValue("ref")) if ref == "" || !git.BranchExists(c.UserContext(), diskPath, ref) { ref, _ = git.EnsureDefaultBranch(c.UserContext(), diskPath, r.DefaultBranch) } filePath := strings.TrimSpace(c.FormValue("path")) message := strings.TrimSpace(c.FormValue("message")) content := c.FormValue("content") backTo := fmt.Sprintf("/%s/%s/new-file?ref=%s", r.OwnerUsername, r.Name, url.QueryEscape(ref)) clean, cerr := git.CleanSubPath(filePath) if cerr != nil || clean == "" { h.setFlashT(c, "flash.path_invalid") return c.Redirect(backTo, fiber.StatusSeeOther) } if blob, rerr := git.ReadBlob(c.UserContext(), diskPath, ref, clean); rerr == nil && blob != nil { h.setFlashT(c, "flash.file_exists") return c.Redirect(backTo+"&path="+url.QueryEscape(clean), fiber.StatusSeeOther) } if message == "" { message = "Add " + clean } if _, werr := git.WriteFileCommit(c.UserContext(), diskPath, ref, clean, []byte(content), message, u.Username, u.Email); werr != nil { h.setFlashT(c, "flash.file_create_failed") return c.Redirect(backTo+"&path="+url.QueryEscape(clean), fiber.StatusSeeOther) } h.setFlashT(c, "flash.file_created") return c.Redirect(fmt.Sprintf("/%s/%s/blob/%s/%s", r.OwnerUsername, r.Name, ref, clean), fiber.StatusSeeOther) } // RepoCreateFolderSubmit commits a .gitkeep placeholder so the new folder is // tracked by git. // POST /:owner/:repo/new-folder func (h *Handler) RepoCreateFolderSubmit(c *fiber.Ctx) error { r, diskPath, err := h.loadAuthorizedRepo(c, true) if err != nil { return nil } u := middleware.CurrentUser(c) ref := strings.TrimSpace(c.FormValue("ref")) if ref == "" || !git.BranchExists(c.UserContext(), diskPath, ref) { ref, _ = git.EnsureDefaultBranch(c.UserContext(), diskPath, r.DefaultBranch) } folder := strings.TrimSpace(c.FormValue("path")) message := strings.TrimSpace(c.FormValue("message")) backTo := fmt.Sprintf("/%s/%s/new-folder?ref=%s", r.OwnerUsername, r.Name, url.QueryEscape(ref)) clean, cerr := git.CleanSubPath(folder) if cerr != nil || clean == "" { h.setFlashT(c, "flash.path_invalid") return c.Redirect(backTo, fiber.StatusSeeOther) } if message == "" { message = "Create folder " + clean } if _, werr := git.WriteFileCommit(c.UserContext(), diskPath, ref, clean+"/.gitkeep", nil, message, u.Username, u.Email); werr != nil { h.setFlashT(c, "flash.folder_create_failed") return c.Redirect(backTo, fiber.StatusSeeOther) } h.setFlashT(c, "flash.folder_created") return c.Redirect(fmt.Sprintf("/%s/%s/tree/%s/%s", r.OwnerUsername, r.Name, ref, clean), fiber.StatusSeeOther) } // Upload limits for a single multipart request. const ( maxUploadFiles = 25 maxUploadTotal = 50 << 20 // 50 MB across all files ) // RepoUploadPage renders the file upload form. // GET /:owner/:repo/upload func (h *Handler) RepoUploadPage(c *fiber.Ctx) error { r, diskPath, err := h.loadAuthorizedRepo(c, true) if err != nil { return nil } ref := strings.TrimSpace(c.Query("ref")) if ref == "" { ref, _ = git.EnsureDefaultBranch(c.UserContext(), diskPath, r.DefaultBranch) } subPath := strings.Trim(strings.ReplaceAll(c.Query("path"), "\\", "/"), "/") data := h.baseData(c, h.tr(c, "file.upload_file")) data["Repo"] = r data["Ref"] = ref data["SubPath"] = subPath data["ActiveTab"] = "code" data["CanWrite"] = true data["MaxFiles"] = maxUploadFiles data["Branches"], _ = git.ListBranches(c.UserContext(), diskPath) return h.render(c, fiber.StatusOK, "file_upload", data) } // RepoUploadSubmit stores uploaded files on the selected branch in a single // commit. Existing files with the same path are replaced. // POST /:owner/:repo/upload func (h *Handler) RepoUploadSubmit(c *fiber.Ctx) error { r, diskPath, err := h.loadAuthorizedRepo(c, true) if err != nil { return nil } u := middleware.CurrentUser(c) ref := strings.TrimSpace(c.FormValue("ref")) if ref == "" || !git.BranchExists(c.UserContext(), diskPath, ref) { ref, _ = git.EnsureDefaultBranch(c.UserContext(), diskPath, r.DefaultBranch) } dest := strings.Trim(strings.ReplaceAll(strings.TrimSpace(c.FormValue("path")), "\\", "/"), "/") backTo := fmt.Sprintf("/%s/%s/upload?ref=%s", r.OwnerUsername, r.Name, url.QueryEscape(ref)) if dest != "" { backTo += "&path=" + url.QueryEscape(dest) } cleanDest := "" if dest != "" { cleanDest, err = git.CleanSubPath(dest) if err != nil { h.setFlashT(c, "flash.path_invalid") return c.Redirect(backTo, fiber.StatusSeeOther) } } form, ferr := c.MultipartForm() if ferr != nil { h.setFlashT(c, "flash.upload_failed") return c.Redirect(backTo, fiber.StatusSeeOther) } headers := form.File["files"] if len(headers) == 0 { h.setFlashT(c, "flash.upload_none") return c.Redirect(backTo, fiber.StatusSeeOther) } if len(headers) > maxUploadFiles { h.setFlashT(c, "flash.upload_too_many", maxUploadFiles) return c.Redirect(backTo, fiber.StatusSeeOther) } files := make([]git.FileToWrite, 0, len(headers)) total := 0 for _, fh := range headers { name := strings.TrimSpace(strings.ReplaceAll(fh.Filename, "\\", "/")) if idx := strings.LastIndex(name, "/"); idx >= 0 { name = name[idx+1:] } if name == "" || name == "." || name == ".." { continue } full := name if cleanDest != "" { full = cleanDest + "/" + name } clean, cerr := git.CleanSubPath(full) if cerr != nil || clean == "" { continue } f, oerr := fh.Open() if oerr != nil { continue } content, rerr := io.ReadAll(io.LimitReader(f, maxUploadTotal+1)) f.Close() if rerr != nil { continue } total += len(content) if total > maxUploadTotal { h.setFlashT(c, "flash.upload_too_big") return c.Redirect(backTo, fiber.StatusSeeOther) } files = append(files, git.FileToWrite{Path: clean, Content: content}) } if len(files) == 0 { h.setFlashT(c, "flash.upload_none") return c.Redirect(backTo, fiber.StatusSeeOther) } message := strings.TrimSpace(c.FormValue("message")) if message == "" { if len(files) == 1 { message = "Add " + files[0].Path } else { message = fmt.Sprintf("Add %d files", len(files)) } } if _, werr := git.WriteFilesCommit(c.UserContext(), diskPath, ref, files, message, u.Username, u.Email); werr != nil { h.setFlashT(c, "flash.upload_failed") return c.Redirect(backTo, fiber.StatusSeeOther) } h.setFlashT(c, "flash.upload_done", len(files)) target := fmt.Sprintf("/%s/%s/tree/%s", r.OwnerUsername, r.Name, ref) if cleanDest != "" { target += "/" + cleanDest } return c.Redirect(target, fiber.StatusSeeOther) }