did something

This commit is contained in:
2026-05-19 22:55:26 +02:00
parent ec9a286d33
commit 2a81bda00e
12 changed files with 5774 additions and 213 deletions
+6 -3
View File
@@ -28,9 +28,12 @@ func (h *InstanceHandler) Get(w http.ResponseWriter, r *http.Request) {
})
}
// sshHost extracts the hostname from InstanceURL. Falls back to the request
// host when InstanceURL is unset (common in local development).
// sshHost resolves the SSH hostname to display in clone URLs.
// Priority: SSH_HOST env var > InstanceURL hostname > request Host header > localhost.
func (h *InstanceHandler) sshHost(r *http.Request) string {
if h.cfg.SSHHost != "" {
return h.cfg.SSHHost
}
if h.cfg.InstanceURL != "" {
if u, err := url.Parse(h.cfg.InstanceURL); err == nil && u.Hostname() != "" {
return u.Hostname()
@@ -41,5 +44,5 @@ func (h *InstanceHandler) sshHost(r *http.Request) string {
if u, err := url.Parse("http://" + host); err == nil {
return u.Hostname()
}
return host
return "localhost"
}
+124 -6
View File
@@ -1,7 +1,10 @@
package handlers
import (
"archive/zip"
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
@@ -653,6 +656,7 @@ func (h *RepoHandler) lookupRepo(w http.ResponseWriter, r *http.Request) (*model
// SearchFiles handles GET /repos/{owner}/{repo}/files?q=...&ref=...
// Returns up to 20 matching file paths (case-insensitive substring match).
// When q is empty, returns all file paths up to 500 (used by the sidebar tree).
func (h *RepoHandler) SearchFiles(w http.ResponseWriter, r *http.Request) {
repo, ok := h.lookupRepo(w, r)
if !ok {
@@ -660,17 +664,17 @@ func (h *RepoHandler) SearchFiles(w http.ResponseWriter, r *http.Request) {
}
query := strings.TrimSpace(r.URL.Query().Get("q"))
if query == "" {
jsonOK(w, []string{})
return
}
ref := r.URL.Query().Get("ref")
if ref == "" {
ref = repo.DefaultBranch
}
files, err := gitdomain.SearchFiles(repo.DiskPath, ref, query, 20)
limit := 20
if query == "" {
limit = 500
}
files, err := gitdomain.SearchFiles(repo.DiskPath, ref, query, limit)
if err != nil {
jsonError(w, "search failed", http.StatusInternalServerError)
return
@@ -680,3 +684,117 @@ func (h *RepoHandler) SearchFiles(w http.ResponseWriter, r *http.Request) {
}
jsonOK(w, files)
}
// UploadFiles handles POST /repos/{owner}/{repo}/upload — multipart upload.
// Accepts multiple regular files (field "file[]") and/or a ZIP archive (field "zip").
// All files are committed in a single git commit.
func (h *RepoHandler) UploadFiles(w http.ResponseWriter, r *http.Request) {
repo, ok := h.lookupRepo(w, r)
if !ok {
return
}
username, _ := r.Context().Value(middleware.ContextKeyUsername).(string)
if !HasPermission(h.db, repo, username, "write") {
jsonError(w, "you do not have write access to this repository", http.StatusForbidden)
return
}
const maxUpload = 50 << 20 // 50 MB
if err := r.ParseMultipartForm(maxUpload); err != nil {
jsonError(w, "could not parse upload: "+err.Error(), http.StatusBadRequest)
return
}
branch := r.FormValue("branch")
if branch == "" {
branch = repo.DefaultBranch
}
message := r.FormValue("message")
if message == "" {
message = "Upload files"
}
var uploads []gitdomain.FileUpload
// Regular files (field "file[]" or "file"). Browser sends webkitRelativePath
// via the custom header X-File-Path; fall back to the bare filename.
for _, fhs := range r.MultipartForm.File {
for _, fh := range fhs {
if fh.Size == 0 {
continue
}
f, err := fh.Open()
if err != nil {
continue
}
data, err := io.ReadAll(io.LimitReader(f, 10<<20)) // 10 MB per file
f.Close()
if err != nil {
continue
}
// Prefer the relative path sent by the browser (folder upload),
// otherwise use the bare filename.
relPath := fh.Filename
if rp := fh.Header.Get("X-File-Path"); rp != "" {
relPath = rp
}
if strings.EqualFold(fh.Header.Get("Content-Disposition"), "") {
// Skip the "zip" field — handled separately below.
}
clean := filepath.Clean(filepath.FromSlash(relPath))
if strings.HasPrefix(clean, "..") || filepath.IsAbs(clean) {
jsonError(w, fmt.Sprintf("invalid path: %s", relPath), http.StatusBadRequest)
return
}
uploads = append(uploads, gitdomain.FileUpload{Path: clean, Content: data})
}
}
// ZIP archive (field "zip").
if zipFHs, ok := r.MultipartForm.File["zip"]; ok && len(zipFHs) > 0 {
fh := zipFHs[0]
f, err := fh.Open()
if err == nil {
zipData, err := io.ReadAll(io.LimitReader(f, maxUpload))
f.Close()
if err == nil {
zr, err := zip.NewReader(bytes.NewReader(zipData), int64(len(zipData)))
if err == nil {
for _, zf := range zr.File {
if zf.FileInfo().IsDir() {
continue
}
clean := filepath.Clean(filepath.FromSlash(zf.Name))
if strings.HasPrefix(clean, "..") || filepath.IsAbs(clean) {
continue
}
rc, err := zf.Open()
if err != nil {
continue
}
data, err := io.ReadAll(io.LimitReader(rc, 10<<20))
rc.Close()
if err != nil {
continue
}
uploads = append(uploads, gitdomain.FileUpload{Path: clean, Content: data})
}
}
}
}
}
if len(uploads) == 0 {
jsonError(w, "no files found in upload", http.StatusBadRequest)
return
}
if err := gitdomain.WriteManyFiles(repo.DiskPath, branch, message, username, username+"@forgebucket", uploads); err != nil {
jsonError(w, "commit failed: "+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]int{"committed": len(uploads)}) //nolint:errcheck
}