rin
Public Open-source lightweight git hosting.
repo_files.go · 268 lines · 8.1 KB
Raw
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