1package main
2
3import (
4 "archive/tar"
5 "compress/gzip"
6 "fmt"
7 "html/template"
8 "io"
9 "log"
10 "net/http"
11 "path"
12 "sort"
13 "strconv"
14 "strings"
15
16 "github.com/go-git/go-git/v5/plumbing"
17 "github.com/go-git/go-git/v5/plumbing/object"
18)
19
20func filesHandler(w http.ResponseWriter, r *http.Request) {
21 ctx, err := getRepoContext(w, r)
22 if err != nil {
23 http.Error(w, err.Error(), http.StatusInternalServerError)
24 return
25 }
26
27 pathValue := r.PathValue("path")
28 commit, err := ctx.GitRepo.CommitObject(ctx.Hash)
29 if err != nil {
30 http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError)
31 return
32 }
33
34 tree, err := commit.Tree()
35 if err != nil {
36 http.Error(w, fmt.Sprintf("Error getting tree: %v", err), http.StatusInternalServerError)
37 return
38 }
39
40 if pathValue != "" {
41 tree, err = tree.Tree(pathValue)
42 if err != nil {
43 http.NotFound(w, r)
44 return
45 }
46 }
47
48 var entries []TreeEntry
49 for _, entry := range tree.Entries {
50 fullPath := entry.Name
51 if pathValue != "" {
52 fullPath = pathValue + "/" + entry.Name
53 }
54
55 isDir := entry.Mode.IsFile() == false
56
57 var size int64
58 if !isDir {
59 obj, _ := ctx.GitRepo.Object(plumbing.AnyObject, entry.Hash)
60 if blob, ok := obj.(*object.Blob); ok {
61 size = blob.Size
62 }
63 }
64
65 entries = append(entries, TreeEntry{
66 Name: entry.Name,
67 Path: fullPath,
68 IsDir: isDir,
69 Size: size,
70 Mode: entry.Mode.String(),
71 })
72 }
73
74 sort.Slice(entries, func(i, j int) bool {
75 if entries[i].IsDir != entries[j].IsDir {
76 return entries[i].IsDir
77 }
78 return entries[i].Name < entries[j].Name
79 })
80
81 data := struct {
82 *RepoContext
83 Entries []TreeEntry
84 Path string
85 View string
86 }{
87 RepoContext: ctx,
88 Entries: entries,
89 Path: pathValue,
90 View: "files",
91 }
92
93 err = templates.ExecuteTemplate(w, "files.html", data)
94 if err != nil {
95 http.Error(w, err.Error(), http.StatusInternalServerError)
96 }
97}
98
99func blobHandler(w http.ResponseWriter, r *http.Request) {
100 ctx, err := getRepoContext(w, r)
101 if err != nil {
102 http.Error(w, err.Error(), http.StatusInternalServerError)
103 return
104 }
105
106 pathValue := r.PathValue("path")
107 commit, err := ctx.GitRepo.CommitObject(ctx.Hash)
108 if err != nil {
109 http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError)
110 return
111 }
112
113 file, err := commit.File(pathValue)
114 if err != nil {
115 http.NotFound(w, r)
116 return
117 }
118
119 isBinary, _ := file.IsBinary()
120 if isBinary {
121 http.Redirect(w, r, fmt.Sprintf("/r/%s/raw/%s?ref=%s", ctx.Repo.Name, pathValue, ctx.CurrentRef), http.StatusFound)
122 return
123 }
124
125 content, err := file.Contents()
126 if err != nil {
127 http.Error(w, fmt.Sprintf("Error reading file: %v", err), http.StatusInternalServerError)
128 return
129 }
130
131 data := struct {
132 *RepoContext
133 Path string
134 Content template.HTML
135 }{
136 RepoContext: ctx,
137 Path: pathValue,
138 Content: highlight(pathValue, content),
139 }
140
141 err = templates.ExecuteTemplate(w, "blob.html", data)
142 if err != nil {
143 http.Error(w, err.Error(), http.StatusInternalServerError)
144 }
145}
146
147func rawHandler(w http.ResponseWriter, r *http.Request) {
148 ctx, err := getRepoContext(w, r)
149 if err != nil {
150 http.Error(w, err.Error(), http.StatusInternalServerError)
151 return
152 }
153
154 pathValue := r.PathValue("path")
155 commit, err := ctx.GitRepo.CommitObject(ctx.Hash)
156 if err != nil {
157 http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError)
158 return
159 }
160
161 file, err := commit.File(pathValue)
162 if err != nil {
163 http.NotFound(w, r)
164 return
165 }
166
167 reader, err := file.Reader()
168 if err != nil {
169 http.Error(w, fmt.Sprintf("Error reading file: %v", err), http.StatusInternalServerError)
170 return
171 }
172 defer reader.Close()
173
174 // Read first 512 bytes to detect content type
175 data := make([]byte, 512)
176 n, err := io.ReadFull(reader, data)
177 if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
178 http.Error(w, fmt.Sprintf("Error reading file: %v", err), http.StatusInternalServerError)
179 return
180 }
181 data = data[:n]
182
183 contentType := http.DetectContentType(data)
184
185 // Fallback for some common types if DetectContentType returns octet-stream or text/plain
186 if contentType == "application/octet-stream" || contentType == "text/plain; charset=utf-8" {
187 ext := strings.ToLower(path.Ext(pathValue))
188 switch ext {
189 case ".go", ".ts", ".js", ".md", ".txt", ".json", ".yaml", ".yml", ".sh", ".c", ".h", ".cpp", ".hpp", ".css", ".html":
190 contentType = "text/plain; charset=utf-8"
191 case ".pdf":
192 contentType = "application/pdf"
193 case ".svg":
194 contentType = "image/svg+xml"
195 }
196 }
197
198 w.Header().Set("Content-Type", contentType)
199 w.Header().Set("Content-Length", strconv.FormatInt(file.Size, 10))
200
201 // Decide if it should be inline or attachment
202 disposition := "inline"
203 if contentType == "application/octet-stream" {
204 disposition = "attachment"
205 }
206
207 w.Header().Set("Content-Disposition", fmt.Sprintf("%s; filename=\"%s\"", disposition, path.Base(pathValue)))
208
209 // Write the first bytes we read
210 w.Write(data)
211 // Then copy the rest
212 io.Copy(w, reader)
213}
214
215func archiveHandler(w http.ResponseWriter, r *http.Request) {
216 ctx, err := getRepoContext(w, r)
217 if err != nil {
218 http.Error(w, err.Error(), http.StatusInternalServerError)
219 return
220 }
221
222 pathValue := r.PathValue("path")
223 commit, err := ctx.GitRepo.CommitObject(ctx.Hash)
224 if err != nil {
225 http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError)
226 return
227 }
228
229 tree, err := commit.Tree()
230 if err != nil {
231 http.Error(w, fmt.Sprintf("Error getting tree: %v", err), http.StatusInternalServerError)
232 return
233 }
234
235 if pathValue != "" {
236 tree, err = tree.Tree(pathValue)
237 if err != nil {
238 http.NotFound(w, r)
239 return
240 }
241 }
242
243 filename := ctx.Repo.Name
244 if pathValue != "" {
245 filename = path.Base(pathValue)
246 }
247 filename = fmt.Sprintf("%s-%s.tar.gz", filename, ctx.CurrentRef)
248
249 w.Header().Set("Content-Type", "application/gzip")
250 w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
251
252 gw := gzip.NewWriter(w)
253 defer gw.Close()
254
255 tw := tar.NewWriter(gw)
256 defer tw.Close()
257
258 err = tree.Files().ForEach(func(f *object.File) error {
259 hdr := &tar.Header{
260 Name: strings.TrimPrefix(strings.TrimPrefix(f.Name, pathValue), "/"),
261 Mode: int64(f.Mode),
262 Size: f.Size,
263 }
264
265 if err := tw.WriteHeader(hdr); err != nil {
266 return err
267 }
268
269 reader, err := f.Reader()
270 if err != nil {
271 return err
272 }
273 defer reader.Close()
274
275 _, err = io.Copy(tw, reader)
276 return err
277 })
278
279 if err != nil {
280 log.Printf("Error creating archive: %v", err)
281 }
282}