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)
+ }
+}