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}