ncDocConverter/internal/ncworker/ncconverter.go

307 lines
8.0 KiB
Go

package ncworker
import (
"bytes"
"encoding/xml"
"fmt"
"io"
"net/http"
"path/filepath"
"strconv"
"strings"
"text/template"
"time"
"rpjosh.de/ncDocConverter/internal/models"
"rpjosh.de/ncDocConverter/pkg/logger"
"rpjosh.de/ncDocConverter/web"
)
type convertJob struct {
job *models.ConvertJob
user *models.User
}
type searchResult struct {
XMLName xml.Name `xml:"multistatus"`
Text string `xml:",chardata"`
D string `xml:"d,attr"`
S string `xml:"s,attr"`
Oc string `xml:"oc,attr"`
Nc string `xml:"nc,attr"`
Response []struct {
Text string `xml:",chardata"`
Href string `xml:"href"`
Propstat struct {
Text string `xml:",chardata"`
Prop struct {
Text string `xml:",chardata"`
Getcontenttype string `xml:"getcontenttype"`
Getlastmodified string `xml:"getlastmodified"`
Size string `xml:"size"`
Fileid int `xml:"fileid"`
} `xml:"prop"`
Status string `xml:"status"`
} `xml:"propstat"`
} `xml:"response"`
}
type ncFiles struct {
extension string
path string
lastModified time.Time
contentType string
size int
fileid int
}
type searchTemplateData struct {
Username string
Directory string
ContentType []string
}
func NewJob(job *models.ConvertJob, user *models.User) *convertJob {
convJob := &convertJob{
job: job,
user: user,
}
return convJob
}
func (job *convertJob) ExecuteJob() {
source := job.searchInDirectory(
job.job.SourceDir,
[]string {
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/msword",
},
)
destination := job.searchInDirectory(
job.job.DestinationDir,
[]string {
"application/pdf",
},
)
preCount := len("/remote.php/dav/files/" + job.user.Username + "/")
// store the files in a map
sourceMap := make(map[string]ncFiles)
destinationMap := make(map[string]ncFiles)
for _, file := range source.Response {
path := file.Href[preCount:]
var extension = filepath.Ext(path)
var name = path[0:len(path)-len(extension)][len(job.job.SourceDir):]
// Time format: Fri, 23 Sep 2022 05:46:31 GMT
time, err := time.Parse("Mon, 02 Jan 2006 15:04:05 GMT", file.Propstat.Prop.Getlastmodified)
if err != nil {
logger.Error("%s", err)
}
size, err := strconv.Atoi(file.Propstat.Prop.Size)
if err != nil {
logger.Error("%s", err)
}
sourceMap[name] = ncFiles{
extension: extension,
path: path,
lastModified: time,
size: size,
contentType: file.Propstat.Prop.Getcontenttype,
fileid: file.Propstat.Prop.Fileid,
}
}
for _, file := range destination.Response {
path := file.Href[preCount:]
var extension = filepath.Ext(path)
var name = path[0:len(path)-len(extension)][len(job.job.DestinationDir):]
time, err := time.Parse("Mon, 02 Jan 2006 15:04:05 GMT", file.Propstat.Prop.Getlastmodified)
if err != nil {
logger.Error("%s", err)
}
size, err := strconv.Atoi(file.Propstat.Prop.Size)
if err != nil {
logger.Error("%s", err)
}
destinationMap[name] = ncFiles{
extension: extension,
path: path,
lastModified: time,
size: size,
contentType: file.Propstat.Prop.Getcontenttype,
fileid: file.Propstat.Prop.Fileid,
}
}
for index, source := range sourceMap {
// check if the file exists in the destination map
if dest, exists := destinationMap[index]; exists {
// compare timestamp and size
if dest.lastModified.Before(source.lastModified) {
job.convertFile(source.path, source.fileid, dest.path)
}
delete(destinationMap, index)
} else {
job.convertFile(
source.path, source.fileid, job.getDestinationDir(source.path),
)
delete(destinationMap, index)
}
}
// delete the files which are not available anymore
for _, dest := range destinationMap {
job.deleteFile(dest.path)
}
}
func (job *convertJob) getDestinationDir(sourceFile string) string {
sourceFile = sourceFile[len(job.job.SourceDir):]
var extension = filepath.Ext(sourceFile)
var name = sourceFile[0:len(sourceFile)-len(extension)]
return job.job.DestinationDir + name + ".pdf"
}
func (job *convertJob) createFoldersRecursively(destinationFile string) {
s := strings.Split(destinationFile, "/")
folderTree := ""
logger.Debug("Creating directory for file '%s'", destinationFile)
// webdav doesn't have an function to create directories recursively
for _, folder := range s[:len(s) - 1] {
folderTree += folder + "/"
client := http.Client{Timeout: 5 * time.Second}
req, err := http.NewRequest("MKCOL", job.user.NextcloudBaseUrl + "/remote.php/dav/files/" + job.user.Username + "/" + folderTree, nil)
if err != nil {
logger.Error("%s", err)
}
req.SetBasicAuth(job.user.Username, job.user.Password)
res, err := client.Do(req)
if err != nil {
logger.Error("%s", err)
}
if (res.StatusCode != 201 && res.StatusCode != 405) {
}
// status code 201 or 405 (already existing)
}
}
func (job *convertJob) convertFile(sourceFile string, sourceid int, destinationFile string) {
logger.Debug("Trying to convert %s (%d) to %s", sourceFile, sourceid, destinationFile)
job.createFoldersRecursively(destinationFile)
client := http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequest(http.MethodGet, job.user.NextcloudBaseUrl + "/apps/onlyoffice/downloadas", nil)
if err != nil {
logger.Error("%s", err)
}
req.SetBasicAuth(job.user.Username, job.user.Password)
q := req.URL.Query()
q.Add("fileId", fmt.Sprint(sourceid))
q.Add("toExtension", "pdf")
req.URL.RawQuery = q.Encode()
res, err := client.Do(req)
if err != nil {
logger.Error("%s", err)
}
// Status Code 200
defer res.Body.Close()
uploadClient := http.Client{Timeout: 10 * time.Second}
uploadReq, err := http.NewRequest(http.MethodPut, job.user.NextcloudBaseUrl + "/remote.php/dav/files/" + job.user.Username + "/" + destinationFile, res.Body)
if err != nil {
logger.Error("%s", err)
}
uploadReq.SetBasicAuth(job.user.Username, job.user.Password)
uploadReq.Header.Set("Content-Type", "application/binary")
res, err = uploadClient.Do(uploadReq)
if err != nil {
logger.Error("%s", err)
}
if (res.StatusCode != 204 && res.StatusCode != 201) {
logger.Error("Failed to create file %s (#%d)", destinationFile, res.StatusCode)
}
// Status Code 201
res.Body.Close()
}
func (job *convertJob) deleteFile(filePath string) {
client := http.Client{Timeout: 5 * time.Second}
req, err := http.NewRequest(http.MethodDelete, job.user.NextcloudBaseUrl + "/remote.php/dav/files/" + job.user.Username + "/" + filePath, nil)
if err != nil {
logger.Error("%s", err)
}
req.SetBasicAuth(job.user.Username, job.user.Password)
res, err := client.Do(req)
if err != nil {
logger.Error("%s", err)
}
if (res.StatusCode != 204) {
logger.Error("Failed to delete file %s (%d)", filePath, res.StatusCode)
}
}
// Searches all doc files in the source directory
func (job *convertJob) searchInDirectory(directory string, contentType []string) *searchResult {
client := http.Client{Timeout: 5 * time.Second}
template, err := template.ParseFS(web.ApiTemplateFiles, "apitemplate/ncsearch.tmpl.xml")
if err != nil {
logger.Error("%s", err)
}
var buf bytes.Buffer
templateData := searchTemplateData{
Username: job.user.Username,
Directory: directory,
ContentType: contentType,
}
if err = template.Execute(&buf, templateData); err != nil {
logger.Error("%s", err)
}
// Status code 207
req, err := http.NewRequest("SEARCH", job.user.NextcloudBaseUrl + "/remote.php/dav/", &buf)
if err != nil {
logger.Error("%s", err)
}
req.SetBasicAuth(job.user.Username, job.user.Password)
req.Header.Set("Content-Type", "application/xml")
res, err := client.Do(req)
if err != nil {
logger.Error("%s", err)
}
defer res.Body.Close()
resBody, err := io.ReadAll(res.Body)
if err != nil {
logger.Error("%s", err)
}
fmt.Print(res.StatusCode)
var result searchResult
if err = xml.Unmarshal(resBody, &result); err != nil {
logger.Error("%s", err)
}
return &result
}