did something
This commit is contained in:
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -184,6 +184,7 @@ func New(cfg *config.Config, engine *xorm.Engine, store sessions.Store, bus even
|
||||
r.Get("/archive", archiveH.Download)
|
||||
r.Get("/insights", insightsH.Get)
|
||||
r.Get("/files", repoH.SearchFiles)
|
||||
r.With(csrf).Post("/upload", repoH.UploadFiles)
|
||||
r.Get("/diff", repoH.Diff)
|
||||
r.Route("/pulls", func(r chi.Router) {
|
||||
r.Get("/", prH.List)
|
||||
|
||||
Reference in New Issue
Block a user