diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8fc0d80 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +uploads/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8fc0d80 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +uploads/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2cb7bca --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM golang:latest as builder +WORKDIR /go/src/git.dm1sh.ru/dm1sh/any2pdf/ +COPY *.go go.mod ./ +RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o app . + +FROM alpine:latest +RUN apk --no-cache --purge add imagemagick libreoffice ttf-dejavu ttf-opensans msttcorefonts-installer ttf-freefont ttf-liberation ttf-droid ttf-inconsolata ttf-font-awesome ttf-mononoki ttf-hack && rm -rf /usr/share/icons && rm -rf /usr/lib/libreoffice/share/gallery && update-ms-fonts && fc-cache -fv +WORKDIR /root/ +COPY --from=builder /go/src/git.dm1sh.ru/dm1sh/any2pdf/app ./ +COPY index.html ./ +ENV PORT=8080 +CMD ["./app"] diff --git a/convert.go b/convert.go new file mode 100644 index 0000000..d06434d --- /dev/null +++ b/convert.go @@ -0,0 +1,61 @@ +package main + +import ( + "fmt" + "os/exec" + "time" +) + +func ConvertToPDF(folderPath string, filesPaths []string, contType ContentType) (resultingPath string, error error) { + switch contType { + case Images: + return ImagesToPDF(folderPath, filesPaths) + case Office: + return OfficeToPDF(folderPath, filesPaths) + default: + return "", fmt.Errorf("Unhandled ContentType with %d index", contType) + } +} + +func ImagesToPDF(folderPath string, filesPaths []string) (resultingPath string, error error) { + resultingPath = createResultingPath(folderPath, "Images") + + if err := runCommand("convert", append(filesPaths, resultingPath)...); err != nil { + return "", err + } + + return resultingPath, nil +} + +func OfficeToPDF(folderPath string, filesPaths []string) (resultingPath string, error error) { + resultingPath = createResultingPath(folderPath, "Office document") + + err := runCommand( + "soffice", "--headless", "--nologo", "--nofirststartwizard", + "--norestore", "--convert-to", "pdf", "--outdir", folderPath, filesPaths[0], + ) + if err != nil { + return "", err + } + + err = runCommand("mv", folderPath + "/" + "0.pdf", resultingPath) + if err != nil { + return "", err + } + + return resultingPath, nil +} + +func createResultingPath(folderPath string, suffix string) string { + return folderPath + "/" + time.Now().Format(time.ANSIC) + " " + suffix + ".pdf" +} + +func runCommand(command string, arg ...string) error { + cmd := exec.Command(command, arg...) + + if err := cmd.Run(); err != nil { + return err + } + + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d487007 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.dm1sh.ru/dm1sh/any2pdf + +go 1.19 diff --git a/index.html b/index.html new file mode 100644 index 0000000..9b7d74a --- /dev/null +++ b/index.html @@ -0,0 +1,14 @@ + + + + + + any2pdf + + +
+ + +
+ + \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..a3b8a68 --- /dev/null +++ b/main.go @@ -0,0 +1,159 @@ +package main + +import ( + "fmt" + "io" + "log" + "mime/multipart" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "time" +) + +func getPort(default_port string) string { + port := os.Getenv("PORT") + + if port == "" { + return default_port + } + + return port +} + +func indexHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "text/html") + http.ServeFile(w, r, "./index.html") +} + +const UPLOADS_FOLDER_PREFIX = "./uploads/" + +func saveFilesOnDisk(files []*multipart.FileHeader) (folderPath string, filesPaths []string, error error) { + // Create folder for uploaded files + + folderPath = UPLOADS_FOLDER_PREFIX + strconv.FormatInt(time.Now().Unix(), 10) + + if err := os.MkdirAll(folderPath, os.ModePerm); err != nil { + return "", filesPaths, err + } + + filesPaths = make([]string, len(files)) + + for i, fileHeader := range files { + // Open file from request + file, err := fileHeader.Open() + if err != nil { + return "", filesPaths, err + } + + defer file.Close() + + // Generate and store file name + + fileName := strconv.Itoa(i) + strings.ToLower(filepath.Ext(fileHeader.Filename)) + + filesPaths[i] = folderPath + "/" + fileName + + // Create file on disk + dst, err := os.Create(filesPaths[i]) + if err != nil { + return "", filesPaths, err + } + + defer dst.Close() + + _, err = io.Copy(dst, file) + if err != nil { + return "", filesPaths, err + } + } + + return folderPath, filesPaths, nil +} + +type ContentType int + +const ( + Images ContentType = iota + Office +) + +func detectContentType(files []*multipart.FileHeader) (contentType ContentType, error error) { + for _, file := range files { + fileExt := strings.ToLower(filepath.Ext(file.Filename)) + switch fileExt { + case ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".odt", ".ods", ".odp": + if len(files) == 1 { + return Office, nil + } else { + return contentType, fmt.Errorf("Expected one document, got %d", len(files)) + } + case ".jpg", ".jpeg", ".png", ".pdf": + contentType = Images + default: + return contentType, fmt.Errorf("%s file extension is unsupported yet", fileExt) + } + } + + return contentType, nil +} + +const MAX_UPLOAD_SIZE = (1 << 27) + +func uploadHandler(w http.ResponseWriter, r *http.Request) { + // Restrict to POST http method + if r.Method != "POST" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Restrict max size of a bunch of files + r.Body = http.MaxBytesReader(w, r.Body, MAX_UPLOAD_SIZE) + + // Parse body + if err := r.ParseMultipartForm(MAX_UPLOAD_SIZE); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + files := r.MultipartForm.File["files"] + + folderPath, filesPaths, err := saveFilesOnDisk(files) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + defer os.RemoveAll(folderPath) + + contType, err := detectContentType(files) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + resFile, err := ConvertToPDF(folderPath, filesPaths, contType) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Disposition", "attachment; filename=\"" + filepath.Base(resFile) + "\"") + + http.ServeFile(w, r, resFile) +} + +func main() { + http.HandleFunc("/", indexHandler) + http.HandleFunc("/upload", uploadHandler) + + port := getPort("80") + + fmt.Printf("Listening on %s\n", port) + + if err := http.ListenAndServe(":"+port, nil); err != nil { + log.Fatal(err) + } +}