repo_files.go
·
268 строк
·
8.1 KB
| 1 | package handlers |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "io" |
| 6 | "net/url" |
| 7 | "strings" |
| 8 | |
| 9 | "polar/internal/git" |
| 10 | "polar/internal/web/middleware" |
| 11 | |
| 12 | "github.com/gofiber/fiber/v2" |
| 13 | ) |
| 14 | |
| 15 | // RepoNewFilePage renders the web form for creating a file. |
| 16 | // GET /:owner/:repo/new-file |
| 17 | func (h *Handler) RepoNewFilePage(c *fiber.Ctx) error { |
| 18 | return h.renderFileForm(c, "file") |
| 19 | } |
| 20 | |
| 21 | // RepoNewFolderPage renders the form for creating a folder. Git cannot store |
| 22 | // empty directories, so the folder is tracked through a .gitkeep placeholder. |
| 23 | // GET /:owner/:repo/new-folder |
| 24 | func (h *Handler) RepoNewFolderPage(c *fiber.Ctx) error { |
| 25 | return h.renderFileForm(c, "folder") |
| 26 | } |
| 27 | |
| 28 | func (h *Handler) renderFileForm(c *fiber.Ctx, mode string) error { |
| 29 | r, diskPath, err := h.loadAuthorizedRepo(c, true) |
| 30 | if err != nil { |
| 31 | return nil |
| 32 | } |
| 33 | |
| 34 | titleKey := "file.new_file" |
| 35 | if mode == "folder" { |
| 36 | titleKey = "file.new_folder" |
| 37 | } |
| 38 | |
| 39 | ref := strings.TrimSpace(c.Query("ref")) |
| 40 | if ref == "" { |
| 41 | ref, _ = git.EnsureDefaultBranch(c.UserContext(), diskPath, r.DefaultBranch) |
| 42 | } |
| 43 | subPath := strings.Trim(strings.ReplaceAll(c.Query("path"), "\\", "/"), "/") |
| 44 | |
| 45 | data := h.baseData(c, h.tr(c, titleKey)) |
| 46 | data["Repo"] = r |
| 47 | data["Ref"] = ref |
| 48 | data["SubPath"] = subPath |
| 49 | data["Mode"] = mode |
| 50 | data["ActiveTab"] = "code" |
| 51 | data["CanWrite"] = true |
| 52 | data["Branches"], _ = git.ListBranches(c.UserContext(), diskPath) |
| 53 | return h.render(c, fiber.StatusOK, "file_new", data) |
| 54 | } |
| 55 | |
| 56 | // RepoCreateFileSubmit commits a new file to the selected branch. |
| 57 | // POST /:owner/:repo/new-file |
| 58 | func (h *Handler) RepoCreateFileSubmit(c *fiber.Ctx) error { |
| 59 | r, diskPath, err := h.loadAuthorizedRepo(c, true) |
| 60 | if err != nil { |
| 61 | return nil |
| 62 | } |
| 63 | u := middleware.CurrentUser(c) |
| 64 | |
| 65 | ref := strings.TrimSpace(c.FormValue("ref")) |
| 66 | if ref == "" || !git.BranchExists(c.UserContext(), diskPath, ref) { |
| 67 | ref, _ = git.EnsureDefaultBranch(c.UserContext(), diskPath, r.DefaultBranch) |
| 68 | } |
| 69 | |
| 70 | filePath := strings.TrimSpace(c.FormValue("path")) |
| 71 | message := strings.TrimSpace(c.FormValue("message")) |
| 72 | content := c.FormValue("content") |
| 73 | |
| 74 | backTo := fmt.Sprintf("/%s/%s/new-file?ref=%s", r.OwnerUsername, r.Name, url.QueryEscape(ref)) |
| 75 | |
| 76 | clean, cerr := git.CleanSubPath(filePath) |
| 77 | if cerr != nil || clean == "" { |
| 78 | h.setFlashT(c, "flash.path_invalid") |
| 79 | return c.Redirect(backTo, fiber.StatusSeeOther) |
| 80 | } |
| 81 | if blob, rerr := git.ReadBlob(c.UserContext(), diskPath, ref, clean); rerr == nil && blob != nil { |
| 82 | h.setFlashT(c, "flash.file_exists") |
| 83 | return c.Redirect(backTo+"&path="+url.QueryEscape(clean), fiber.StatusSeeOther) |
| 84 | } |
| 85 | if message == "" { |
| 86 | message = "Add " + clean |
| 87 | } |
| 88 | if _, werr := git.WriteFileCommit(c.UserContext(), diskPath, ref, clean, []byte(content), message, u.Username, u.Email); werr != nil { |
| 89 | h.setFlashT(c, "flash.file_create_failed") |
| 90 | return c.Redirect(backTo+"&path="+url.QueryEscape(clean), fiber.StatusSeeOther) |
| 91 | } |
| 92 | |
| 93 | h.setFlashT(c, "flash.file_created") |
| 94 | return c.Redirect(fmt.Sprintf("/%s/%s/blob/%s/%s", r.OwnerUsername, r.Name, ref, clean), fiber.StatusSeeOther) |
| 95 | } |
| 96 | |
| 97 | // RepoCreateFolderSubmit commits a .gitkeep placeholder so the new folder is |
| 98 | // tracked by git. |
| 99 | // POST /:owner/:repo/new-folder |
| 100 | func (h *Handler) RepoCreateFolderSubmit(c *fiber.Ctx) error { |
| 101 | r, diskPath, err := h.loadAuthorizedRepo(c, true) |
| 102 | if err != nil { |
| 103 | return nil |
| 104 | } |
| 105 | u := middleware.CurrentUser(c) |
| 106 | |
| 107 | ref := strings.TrimSpace(c.FormValue("ref")) |
| 108 | if ref == "" || !git.BranchExists(c.UserContext(), diskPath, ref) { |
| 109 | ref, _ = git.EnsureDefaultBranch(c.UserContext(), diskPath, r.DefaultBranch) |
| 110 | } |
| 111 | |
| 112 | folder := strings.TrimSpace(c.FormValue("path")) |
| 113 | message := strings.TrimSpace(c.FormValue("message")) |
| 114 | |
| 115 | backTo := fmt.Sprintf("/%s/%s/new-folder?ref=%s", r.OwnerUsername, r.Name, url.QueryEscape(ref)) |
| 116 | |
| 117 | clean, cerr := git.CleanSubPath(folder) |
| 118 | if cerr != nil || clean == "" { |
| 119 | h.setFlashT(c, "flash.path_invalid") |
| 120 | return c.Redirect(backTo, fiber.StatusSeeOther) |
| 121 | } |
| 122 | if message == "" { |
| 123 | message = "Create folder " + clean |
| 124 | } |
| 125 | if _, werr := git.WriteFileCommit(c.UserContext(), diskPath, ref, clean+"/.gitkeep", nil, message, u.Username, u.Email); werr != nil { |
| 126 | h.setFlashT(c, "flash.folder_create_failed") |
| 127 | return c.Redirect(backTo, fiber.StatusSeeOther) |
| 128 | } |
| 129 | |
| 130 | h.setFlashT(c, "flash.folder_created") |
| 131 | return c.Redirect(fmt.Sprintf("/%s/%s/tree/%s/%s", r.OwnerUsername, r.Name, ref, clean), fiber.StatusSeeOther) |
| 132 | } |
| 133 | |
| 134 | // Upload limits for a single multipart request. |
| 135 | const ( |
| 136 | maxUploadFiles = 25 |
| 137 | maxUploadTotal = 50 << 20 // 50 MB across all files |
| 138 | ) |
| 139 | |
| 140 | // RepoUploadPage renders the file upload form. |
| 141 | // GET /:owner/:repo/upload |
| 142 | func (h *Handler) RepoUploadPage(c *fiber.Ctx) error { |
| 143 | r, diskPath, err := h.loadAuthorizedRepo(c, true) |
| 144 | if err != nil { |
| 145 | return nil |
| 146 | } |
| 147 | |
| 148 | ref := strings.TrimSpace(c.Query("ref")) |
| 149 | if ref == "" { |
| 150 | ref, _ = git.EnsureDefaultBranch(c.UserContext(), diskPath, r.DefaultBranch) |
| 151 | } |
| 152 | subPath := strings.Trim(strings.ReplaceAll(c.Query("path"), "\\", "/"), "/") |
| 153 | |
| 154 | data := h.baseData(c, h.tr(c, "file.upload_file")) |
| 155 | data["Repo"] = r |
| 156 | data["Ref"] = ref |
| 157 | data["SubPath"] = subPath |
| 158 | data["ActiveTab"] = "code" |
| 159 | data["CanWrite"] = true |
| 160 | data["MaxFiles"] = maxUploadFiles |
| 161 | data["Branches"], _ = git.ListBranches(c.UserContext(), diskPath) |
| 162 | return h.render(c, fiber.StatusOK, "file_upload", data) |
| 163 | } |
| 164 | |
| 165 | // RepoUploadSubmit stores uploaded files on the selected branch in a single |
| 166 | // commit. Existing files with the same path are replaced. |
| 167 | // POST /:owner/:repo/upload |
| 168 | func (h *Handler) RepoUploadSubmit(c *fiber.Ctx) error { |
| 169 | r, diskPath, err := h.loadAuthorizedRepo(c, true) |
| 170 | if err != nil { |
| 171 | return nil |
| 172 | } |
| 173 | u := middleware.CurrentUser(c) |
| 174 | |
| 175 | ref := strings.TrimSpace(c.FormValue("ref")) |
| 176 | if ref == "" || !git.BranchExists(c.UserContext(), diskPath, ref) { |
| 177 | ref, _ = git.EnsureDefaultBranch(c.UserContext(), diskPath, r.DefaultBranch) |
| 178 | } |
| 179 | dest := strings.Trim(strings.ReplaceAll(strings.TrimSpace(c.FormValue("path")), "\\", "/"), "/") |
| 180 | |
| 181 | backTo := fmt.Sprintf("/%s/%s/upload?ref=%s", r.OwnerUsername, r.Name, url.QueryEscape(ref)) |
| 182 | if dest != "" { |
| 183 | backTo += "&path=" + url.QueryEscape(dest) |
| 184 | } |
| 185 | |
| 186 | cleanDest := "" |
| 187 | if dest != "" { |
| 188 | cleanDest, err = git.CleanSubPath(dest) |
| 189 | if err != nil { |
| 190 | h.setFlashT(c, "flash.path_invalid") |
| 191 | return c.Redirect(backTo, fiber.StatusSeeOther) |
| 192 | } |
| 193 | } |
| 194 | |
| 195 | form, ferr := c.MultipartForm() |
| 196 | if ferr != nil { |
| 197 | h.setFlashT(c, "flash.upload_failed") |
| 198 | return c.Redirect(backTo, fiber.StatusSeeOther) |
| 199 | } |
| 200 | headers := form.File["files"] |
| 201 | if len(headers) == 0 { |
| 202 | h.setFlashT(c, "flash.upload_none") |
| 203 | return c.Redirect(backTo, fiber.StatusSeeOther) |
| 204 | } |
| 205 | if len(headers) > maxUploadFiles { |
| 206 | h.setFlashT(c, "flash.upload_too_many", maxUploadFiles) |
| 207 | return c.Redirect(backTo, fiber.StatusSeeOther) |
| 208 | } |
| 209 | |
| 210 | files := make([]git.FileToWrite, 0, len(headers)) |
| 211 | total := 0 |
| 212 | for _, fh := range headers { |
| 213 | name := strings.TrimSpace(strings.ReplaceAll(fh.Filename, "\\", "/")) |
| 214 | if idx := strings.LastIndex(name, "/"); idx >= 0 { |
| 215 | name = name[idx+1:] |
| 216 | } |
| 217 | if name == "" || name == "." || name == ".." { |
| 218 | continue |
| 219 | } |
| 220 | full := name |
| 221 | if cleanDest != "" { |
| 222 | full = cleanDest + "/" + name |
| 223 | } |
| 224 | clean, cerr := git.CleanSubPath(full) |
| 225 | if cerr != nil || clean == "" { |
| 226 | continue |
| 227 | } |
| 228 | f, oerr := fh.Open() |
| 229 | if oerr != nil { |
| 230 | continue |
| 231 | } |
| 232 | content, rerr := io.ReadAll(io.LimitReader(f, maxUploadTotal+1)) |
| 233 | f.Close() |
| 234 | if rerr != nil { |
| 235 | continue |
| 236 | } |
| 237 | total += len(content) |
| 238 | if total > maxUploadTotal { |
| 239 | h.setFlashT(c, "flash.upload_too_big") |
| 240 | return c.Redirect(backTo, fiber.StatusSeeOther) |
| 241 | } |
| 242 | files = append(files, git.FileToWrite{Path: clean, Content: content}) |
| 243 | } |
| 244 | if len(files) == 0 { |
| 245 | h.setFlashT(c, "flash.upload_none") |
| 246 | return c.Redirect(backTo, fiber.StatusSeeOther) |
| 247 | } |
| 248 | |
| 249 | message := strings.TrimSpace(c.FormValue("message")) |
| 250 | if message == "" { |
| 251 | if len(files) == 1 { |
| 252 | message = "Add " + files[0].Path |
| 253 | } else { |
| 254 | message = fmt.Sprintf("Add %d files", len(files)) |
| 255 | } |
| 256 | } |
| 257 | if _, werr := git.WriteFilesCommit(c.UserContext(), diskPath, ref, files, message, u.Username, u.Email); werr != nil { |
| 258 | h.setFlashT(c, "flash.upload_failed") |
| 259 | return c.Redirect(backTo, fiber.StatusSeeOther) |
| 260 | } |
| 261 | |
| 262 | h.setFlashT(c, "flash.upload_done", len(files)) |
| 263 | target := fmt.Sprintf("/%s/%s/tree/%s", r.OwnerUsername, r.Name, ref) |
| 264 | if cleanDest != "" { |
| 265 | target += "/" + cleanDest |
| 266 | } |
| 267 | return c.Redirect(target, fiber.StatusSeeOther) |
| 268 | } |
| 269 |