253 lines
7.2 KiB
Go
253 lines
7.2 KiB
Go
package nextcloud
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
"git.rpjosh.de/RPJosh/go-logger"
|
|
"git.rpjosh.de/ncDocConverter/internal/models"
|
|
"git.rpjosh.de/ncDocConverter/web"
|
|
)
|
|
|
|
// The internal representation of a nextcloud file
|
|
type NcFile struct {
|
|
// File extension: txt
|
|
Extension string
|
|
// Relative path of the file to the nextcloud root: /folder/file.txt
|
|
Path string
|
|
LastModified time.Time
|
|
ContentType string
|
|
// Size in Bytes
|
|
Size int
|
|
// The unique file ID of the nextcloud server
|
|
Fileid int
|
|
// The Webdav URL for file reference
|
|
WebdavURL string
|
|
}
|
|
|
|
type searchTemplateData struct {
|
|
Username string
|
|
Directory string
|
|
ContentType []string
|
|
}
|
|
|
|
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 []searchResultResponse `xml:"response"`
|
|
}
|
|
type searchResultResponse 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"`
|
|
}
|
|
|
|
func (r *searchResultResponse) GetLastModified() time.Time {
|
|
// Time format: Fri, 23 Sep 2022 05:46:31 GMT
|
|
rtc, err := time.Parse("Mon, 02 Jan 2006 15:04:05 GMT", r.Propstat.Prop.Getlastmodified)
|
|
if err != nil {
|
|
logger.Warning("%s", err)
|
|
rtc = time.Unix(0, 1)
|
|
}
|
|
|
|
return rtc
|
|
}
|
|
|
|
// Returns a new request to the Nexcloud API.
|
|
// The path beginning AFTER /dav/ should be given (e.g.: myUser/folder/file.txt)
|
|
func getRequest(method string, path string, body io.Reader, ncUser *models.NextcloudUser) *http.Request {
|
|
req, err := http.NewRequest(method, ncUser.NextcloudBaseUrl+"/remote.php/dav/"+path, body)
|
|
if err != nil {
|
|
logger.Error("%s", err)
|
|
}
|
|
req.SetBasicAuth(ncUser.Username, ncUser.Password)
|
|
|
|
return req
|
|
}
|
|
|
|
// Searches for all files of the given content type starting in the given directory.
|
|
func SearchInDirectory(ncUser *models.NextcloudUser, directory string, contentType []string) (*searchResult, error) {
|
|
client := http.Client{Timeout: 5 * time.Second}
|
|
|
|
template, err := template.ParseFS(web.ApiTemplateFiles, "apitemplate/ncsearch.tmpl.xml")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var buf bytes.Buffer
|
|
templateData := searchTemplateData{
|
|
Username: ncUser.Username,
|
|
Directory: directory,
|
|
ContentType: contentType,
|
|
}
|
|
if err = template.Execute(&buf, templateData); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Status code 207
|
|
req := getRequest("SEARCH", "", &buf, ncUser)
|
|
req.Header.Set("Content-Type", "application/xml")
|
|
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Decody body first before checking status code to print in error message
|
|
defer res.Body.Close()
|
|
|
|
resBody, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Create folder if not existing
|
|
if res.StatusCode == 404 {
|
|
logger.Info("Creating directory '%s' because it does not exist", "/"+directory)
|
|
CreateFoldersRecursively(ncUser, "/"+directory+"notExisting.pdf")
|
|
return &searchResult{}, nil
|
|
}
|
|
|
|
if res.StatusCode != 207 {
|
|
return nil, fmt.Errorf("status code %d: %s", res.StatusCode, resBody)
|
|
}
|
|
|
|
var result searchResult
|
|
if err = xml.Unmarshal(resBody, &result); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &result, nil
|
|
}
|
|
|
|
// Parses the response from the given search format to an NcFile.
|
|
// A map with the relative path based on the source Directory ("someFolder/file.txt")
|
|
// and the mathing NcFile will be returned. Therefore, also the source Directory has to be given.
|
|
//
|
|
// To determine the path without the prefix "/remote.php/dav/user/" it has to be given.
|
|
func ParseSearchResult(result *searchResult, prefix string, sourceDir string) map[string]NcFile {
|
|
preCount := len(prefix)
|
|
rtc := make(map[string]NcFile)
|
|
|
|
for _, file := range result.Response {
|
|
href, _ := url.QueryUnescape(file.Href)
|
|
path := href[preCount:]
|
|
var extension = filepath.Ext(path)
|
|
var name = path[0 : len(path)-len(extension)][len(sourceDir):]
|
|
time := file.GetLastModified()
|
|
size, err := strconv.Atoi(file.Propstat.Prop.Size)
|
|
if err != nil {
|
|
logger.Error("Failed to parse the file size '%s' to an integer: %s", file.Propstat.Prop.Size, err)
|
|
continue
|
|
}
|
|
rtc[name] = NcFile{
|
|
Extension: extension,
|
|
Path: path,
|
|
LastModified: time,
|
|
Size: size,
|
|
ContentType: file.Propstat.Prop.Getcontenttype,
|
|
Fileid: file.Propstat.Prop.Fileid,
|
|
WebdavURL: file.Href,
|
|
}
|
|
}
|
|
|
|
return rtc
|
|
}
|
|
|
|
// Delets a file with the given path.
|
|
// The path has to start at the root level: Ebook/myFolder/file.txt
|
|
func DeleteFile(ncUser *models.NextcloudUser, filePath string) error {
|
|
return deleteFile(ncUser, filePath, true)
|
|
}
|
|
func deleteFile(ncUser *models.NextcloudUser, filePath string, retry bool) error {
|
|
client := http.Client{Timeout: 5 * time.Second}
|
|
|
|
req := getRequest(http.MethodDelete, "files/"+ncUser.Username+"/"+filePath, nil, ncUser)
|
|
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
logger.Error("%s", err)
|
|
}
|
|
|
|
if res.StatusCode != 204 {
|
|
return fmt.Errorf("failed to delete file %s (%d)", filePath, res.StatusCode)
|
|
}
|
|
|
|
// If the server is locked try to delete it again
|
|
if res.StatusCode == 423 && retry {
|
|
logger.Debug("Trying to delete the file again (it was locked previously)")
|
|
time.Sleep(10 * time.Millisecond)
|
|
return deleteFile(ncUser, filePath, false)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Creates all required directorys to create the destination file recursively.
|
|
// The path should be relative to the root: ebook/folder1/folder2/file.txt
|
|
func CreateFoldersRecursively(ncUser *models.NextcloudUser, destinationFile string) {
|
|
s := strings.Split(destinationFile, "/")
|
|
folderTree := ""
|
|
|
|
// Webdav doesn't have a function to create directories recursively → iterate
|
|
for _, folder := range s[:len(s)-1] {
|
|
folderTree += folder + "/"
|
|
|
|
client := http.Client{Timeout: 5 * time.Second}
|
|
req := getRequest("MKCOL", "files/"+ncUser.Username+"/"+folderTree, nil, ncUser)
|
|
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
logger.Error("%s", err)
|
|
}
|
|
|
|
if res.StatusCode != 201 && res.StatusCode != 405 {
|
|
logger.Error("Failed to create directory '%s'", folderTree)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Uploads a file to the nextcloud server.
|
|
// It will be saved to the destination as a relative path to the nextcloud root (ebook/file.txt).
|
|
func UploadFile(ncUser *models.NextcloudUser, destination string, content io.ReadCloser) error {
|
|
client := http.Client{Timeout: 10 * time.Second}
|
|
cnt, err := io.ReadAll(content)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read body: %s", err)
|
|
}
|
|
req := getRequest(http.MethodPut, "files/"+ncUser.Username+"/"+destination, bytes.NewBuffer(cnt), ncUser)
|
|
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if res.StatusCode != 201 && res.StatusCode != 204 {
|
|
return fmt.Errorf("expected status code 201 or 204 but got %d", res.StatusCode)
|
|
}
|
|
|
|
return nil
|
|
}
|