Add file upload on gist creation/edition (#507)

هذا الالتزام موجود في:
Thomas Miceli
2025-09-16 01:56:38 +02:00
ملتزم من قبل GitHub
الأصل 594d876ba8
التزام 53ce41e0e4
12 ملفات معدلة مع 478 إضافات و77 حذوفات

عرض الملف

@@ -487,8 +487,14 @@ func (gist *Gist) AddAndCommitFiles(files *[]FileDTO) error {
}
for _, file := range *files {
if err := git.SetFileContent(gist.Uuid, file.Filename, file.Content); err != nil {
return err
if file.SourcePath != "" { // if it's an uploaded file
if err := git.MoveFileToRepository(gist.Uuid, file.Filename, file.SourcePath); err != nil {
return err
}
} else { // else it's a text editor file
if err := git.SetFileContent(gist.Uuid, file.Filename, file.Content); err != nil {
return err
}
}
}
@@ -546,19 +552,28 @@ func (gist *Gist) UpdatePreviewAndCount(withTimestampUpdate bool) error {
gist.Preview = ""
gist.PreviewFilename = ""
} else {
file, err := gist.File("HEAD", filesStr[0], true)
if err != nil {
return err
}
for _, fileStr := range filesStr {
file, err := gist.File("HEAD", fileStr, true)
if err != nil {
return err
}
if file == nil {
continue
}
gist.Preview = ""
gist.PreviewFilename = file.Filename
split := strings.Split(file.Content, "\n")
if len(split) > 10 {
gist.Preview = strings.Join(split[:10], "\n")
} else {
gist.Preview = file.Content
}
if !file.MimeType.CanBeEdited() {
continue
}
gist.PreviewFilename = file.Filename
split := strings.Split(file.Content, "\n")
if len(split) > 10 {
gist.Preview = strings.Join(split[:10], "\n")
} else {
gist.Preview = file.Content
}
}
}
if withTimestampUpdate {
@@ -721,9 +736,10 @@ type VisibilityDTO struct {
}
type FileDTO struct {
Filename string `validate:"excludes=\x2f,excludes=\x5c,max=255"`
Content string `validate:"required"`
Binary bool
Filename string `validate:"excludes=\x2f,excludes=\x5c,max=255"`
Content string
Binary bool
SourcePath string // Path to uploaded file, used instead of Content when present
}
func (dto *GistDTO) ToGist() *Gist {

عرض الملف

@@ -380,6 +380,17 @@ func SetFileContent(gistTmpId string, filename string, content string) error {
return os.WriteFile(filepath.Join(repositoryPath, filename), []byte(content), 0644)
}
func MoveFileToRepository(gistTmpId string, filename string, sourcePath string) error {
repositoryPath := TmpRepositoryPath(gistTmpId)
destPath := filepath.Join(repositoryPath, filename)
if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil {
return err
}
return os.Rename(sourcePath, destPath)
}
func AddAll(gistTmpId string) error {
tmpPath := TmpRepositoryPath(gistTmpId)

عرض الملف

@@ -28,6 +28,7 @@ gist.file-binary-edit: This file is binary.
gist.watch-full-file: View the full file.
gist.file-not-valid: This file is not a valid CSV file.
gist.no-content: No files found
gist.preview-non-available: Preview not available
gist.new.new_gist: New gist
gist.new.title: Title
@@ -48,6 +49,8 @@ gist.new.create-private-button: Create private gist
gist.new.preview: Preview
gist.new.create-a-new-gist: Create a new gist
gist.new.topics: Topics (separate with spaces)
gist.new.drop-files: Drop files here or click to upload
gist.new.any-file-type: Upload any file type
gist.edit.editing: Editing
gist.edit.edit-gist: Edit %s
@@ -218,6 +221,8 @@ error.cannot-bind-data: Cannot bind data
error.invalid-number: Invalid number
error.invalid-character-unescaped: Invalid character unescaped
error.not-in-mfa-session: User is not in a MFA session
error.no-file-uploaded: No file uploaded
error.cannot-open-file: Cannot open uploaded file
header.menu.all: All
header.menu.new: New

عرض الملف

@@ -2,11 +2,15 @@ package gist
import (
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/google/uuid"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/git"
"github.com/thomiceli/opengist/internal/i18n"
"github.com/thomiceli/opengist/internal/validator"
"github.com/thomiceli/opengist/internal/web/context"
@@ -44,10 +48,16 @@ func ProcessCreate(ctx *context.Context) error {
dto.Files = make([]db.FileDTO, 0)
fileCounter := 0
for i := 0; i < len(ctx.Request().PostForm["content"]); i++ {
name := ctx.Request().PostForm["name"][i]
content := ctx.Request().PostForm["content"][i]
names := ctx.Request().PostForm["name"]
contents := ctx.Request().PostForm["content"]
// Process files from text editors
for i, content := range contents {
if content == "" {
continue
}
name := names[i]
if name == "" {
fileCounter += 1
name = "gistfile" + strconv.Itoa(fileCounter) + ".txt"
@@ -59,10 +69,57 @@ func ProcessCreate(ctx *context.Context) error {
}
dto.Files = append(dto.Files, db.FileDTO{
Filename: strings.Trim(name, " "),
Filename: strings.TrimSpace(name),
Content: escapedValue,
})
}
// Process uploaded files from UUID arrays
fileUUIDs := ctx.Request().PostForm["uploadedfile_uuid"]
fileFilenames := ctx.Request().PostForm["uploadedfile_filename"]
if len(fileUUIDs) == len(fileFilenames) {
for i, fileUUID := range fileUUIDs {
filePath := filepath.Join(filepath.Join(config.GetHomeDir(), "uploads"), fileUUID)
if _, err := os.Stat(filePath); err != nil {
continue
}
dto.Files = append(dto.Files, db.FileDTO{
Filename: fileFilenames[i],
SourcePath: filePath,
Content: "", // Empty since we're using SourcePath
})
}
}
// Process binary file operations (edit mode)
binaryOldNames := ctx.Request().PostForm["binary_old_name"]
binaryNewNames := ctx.Request().PostForm["binary_new_name"]
if len(binaryOldNames) == len(binaryNewNames) {
for i, oldName := range binaryOldNames {
newName := binaryNewNames[i]
if newName == "" { // deletion
continue
}
if !isCreate {
gistOld := ctx.GetData("gist").(*db.Gist)
fileContent, _, err := git.GetFileContent(gistOld.User.Username, gistOld.Uuid, "HEAD", oldName, false)
if err != nil {
continue
}
dto.Files = append(dto.Files, db.FileDTO{
Filename: newName,
Content: fileContent,
Binary: true,
})
}
}
}
ctx.SetData("dto", dto)
err = ctx.Validate(dto)
@@ -101,24 +158,13 @@ func ProcessCreate(ctx *context.Context) error {
}
if gist.Title == "" {
if ctx.Request().PostForm["name"][0] == "" {
if dto.Files[0].Filename == "" {
gist.Title = "gist:" + gist.Uuid
} else {
gist.Title = ctx.Request().PostForm["name"][0]
gist.Title = dto.Files[0].Filename
}
}
if len(dto.Files) > 0 {
split := strings.Split(dto.Files[0].Content, "\n")
if len(split) > 10 {
gist.Preview = strings.Join(split[:10], "\n")
} else {
gist.Preview = dto.Files[0].Content
}
gist.PreviewFilename = dto.Files[0].Filename
}
if err = gist.InitRepository(); err != nil {
return ctx.ErrorRes(500, "Error creating the repository", err)
}
@@ -139,6 +185,9 @@ func ProcessCreate(ctx *context.Context) error {
gist.AddInIndex()
gist.UpdateLanguages()
if err = gist.UpdatePreviewAndCount(true); err != nil {
return ctx.ErrorRes(500, "Error updating preview and count", err)
}
return ctx.RedirectTo("/" + user.Username + "/" + gist.Identifier())
}

عرض الملف

@@ -0,0 +1,77 @@
package gist
import (
"io"
"os"
"path/filepath"
"github.com/google/uuid"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/web/context"
)
func Upload(ctx *context.Context) error {
err := ctx.Request().ParseMultipartForm(32 << 20) // 32 MB max
if err != nil {
return ctx.ErrorRes(400, ctx.Tr("error.bad-request"), err)
}
fileHeader, err := ctx.FormFile("file")
if err != nil {
return ctx.ErrorRes(400, ctx.Tr("error.no-file-uploaded"), err)
}
file, err := fileHeader.Open()
if err != nil {
return ctx.ErrorRes(400, ctx.Tr("error.cannot-open-file"), err)
}
defer file.Close()
fileUUID, err := uuid.NewRandom()
if err != nil {
return ctx.ErrorRes(500, "Error generating UUID", err)
}
uploadsDir := filepath.Join(config.GetHomeDir(), "uploads")
if err := os.MkdirAll(uploadsDir, 0755); err != nil {
return ctx.ErrorRes(500, "Error creating uploads directory", err)
}
filename := fileUUID.String()
filePath := filepath.Join(uploadsDir, filename)
destFile, err := os.Create(filePath)
if err != nil {
return ctx.ErrorRes(500, "Error creating file", err)
}
defer destFile.Close()
if _, err := io.Copy(destFile, file); err != nil {
return ctx.ErrorRes(500, "Error saving file", err)
}
return ctx.JSON(200, map[string]string{
"uuid": filename,
"filename": fileHeader.Filename,
})
}
func DeleteUpload(ctx *context.Context) error {
uuid := ctx.Param("uuid")
if uuid == "" {
return ctx.ErrorRes(400, ctx.Tr("error.bad-request"), nil)
}
uploadsDir := filepath.Join(config.GetHomeDir(), "uploads")
filePath := filepath.Join(uploadsDir, uuid)
if _, err := os.Stat(filePath); err == nil {
if err := os.Remove(filePath); err != nil {
return ctx.ErrorRes(500, "Error deleting file", err)
}
}
return ctx.JSON(200, map[string]string{
"status": "deleted",
})
}

عرض الملف

@@ -29,6 +29,8 @@ func (s *Server) registerRoutes() {
r.GET("/", gist.Create, logged)
r.POST("/", gist.ProcessCreate, logged)
r.POST("/preview", gist.Preview, logged)
r.POST("/upload", gist.Upload, logged)
r.DELETE("/upload/:uuid", gist.DeleteUpload, logged)
r.GET("/healthcheck", health.Healthcheck)

عرض الملف

@@ -66,7 +66,7 @@ func TestGists(t *testing.T) {
Content: []string{"", "yeah\ncool", "yeah\ncool gist actually"},
Topics: "",
}
err = s.Request("POST", "/", gist2, 400)
err = s.Request("POST", "/", gist2, 302)
require.NoError(t, err)
gist3 := db.GistDTO{
@@ -82,7 +82,7 @@ func TestGists(t *testing.T) {
err = s.Request("POST", "/", gist3, 302)
require.NoError(t, err)
gist3db, err := db.GetGistByID("2")
gist3db, err := db.GetGistByID("3")
require.NoError(t, err)
gist3files, err := git.GetFilesOfRepository(gist3db.User.Username, gist3db.Uuid, "HEAD")