package nextcloud import ( "bytes" "encoding/xml" "fmt" "io" "net/http" "strings" "text/template" "time" "rpjosh.de/ncDocConverter/internal/models" "rpjosh.de/ncDocConverter/pkg/logger" "rpjosh.de/ncDocConverter/web" ) 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 } 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 } // 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 { 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) } 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 directorys") } } } // 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: 5 * time.Second} req := getRequest(http.MethodPut, "files/"+ncUser.Username+"/"+destination, content, 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 }