package storage import ( "bytes" "crypto/tls" "io" "net/http" "path/filepath" "strings" "time" "github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/log" "github.com/mayswind/ezbookkeeping/pkg/settings" "github.com/mayswind/ezbookkeeping/pkg/utils" ) // WebDAVObjectStorage represents WebDAV object storage type WebDAVObjectStorage struct { httpClient *http.Client webDavConfig *settings.WebDAVConfig rootPath string } // NewWebDAVObjectStorage returns a WebDAV object storage func NewWebDAVObjectStorage(config *settings.Config, pathPrefix string) (*WebDAVObjectStorage, error) { webDavConfig := config.WebDAVConfig transport := http.DefaultTransport.(*http.Transport).Clone() utils.SetProxyUrl(transport, webDavConfig.Proxy) if webDavConfig.SkipTLSVerify { transport.TLSClientConfig = &tls.Config{ InsecureSkipVerify: true, } } client := &http.Client{ Transport: transport, Timeout: time.Duration(webDavConfig.RequestTimeout) * time.Millisecond, } storage := &WebDAVObjectStorage{ httpClient: client, webDavConfig: webDavConfig, rootPath: webDavConfig.RootPath, } storage.rootPath = storage.getFinalPath(pathPrefix) storage.rootPath = strings.ReplaceAll(storage.rootPath, "\\", "/") ctx := core.NewNullContext() exists, err := storage.directoryExists(ctx, storage.rootPath) if err != nil { return nil, err } if !exists { err := storage.createAllDirectories(ctx, "", storage.rootPath) if err != nil { return nil, err } } return storage, nil } // Exists returns whether the file exists func (s *WebDAVObjectStorage) Exists(ctx core.Context, path string) (bool, error) { req, err := http.NewRequest("HEAD", s.getFinalFileUrl(path), nil) if err != nil { return false, err } req.SetBasicAuth(s.webDavConfig.Username, s.webDavConfig.Password) resp, err := s.httpClient.Do(req) if err != nil { log.Errorf(ctx, "[webdav_storage.Exists] cannot check file exists, because %s", err.Error()) return false, err } if resp.StatusCode == http.StatusOK { return true, nil } else if resp.StatusCode == http.StatusNotFound { return false, nil } log.Errorf(ctx, "[webdav_storage.Exists] cannot check file exists, http status code is %d", resp.StatusCode) return false, errs.ErrSystemError } // Read returns the object instance according to specified the file path func (s *WebDAVObjectStorage) Read(ctx core.Context, path string) (ObjectInStorage, error) { req, err := http.NewRequest("GET", s.getFinalFileUrl(path), nil) if err != nil { return nil, err } req.SetBasicAuth(s.webDavConfig.Username, s.webDavConfig.Password) resp, err := s.httpClient.Do(req) if err != nil { log.Errorf(ctx, "[webdav_storage.Read] cannot get file, because %s", err.Error()) return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { log.Errorf(ctx, "[webdav_storage.Read] cannot read response (http status code %d) body, because %s", resp.StatusCode, err.Error()) return nil, err } if resp.StatusCode != http.StatusOK { log.Errorf(ctx, "[webdav_storage.Read] cannot get file, http status code is %d, response is %s", resp.StatusCode, string(body)) return nil, errs.ErrSystemError } return newByteSliceObject(body), nil } // Save returns whether save the object instance successfully func (s *WebDAVObjectStorage) Save(ctx core.Context, path string, object ObjectInStorage) error { finalPath := s.getFinalPath(path) dir := strings.ReplaceAll(filepath.Dir(finalPath), "\\", "/") exists, err := s.directoryExists(ctx, dir) if err != nil { return err } if !exists { rootExists, err := s.directoryExists(ctx, s.rootPath) if err != nil { return err } if !rootExists { err := s.createAllDirectories(ctx, "", s.rootPath) if err != nil { return err } } err = s.createAllDirectories(ctx, s.rootPath, strings.ReplaceAll(filepath.Dir(path), "\\", "/")) if err != nil { return err } } data, err := io.ReadAll(object) if err != nil { return err } req, err := http.NewRequest("PUT", s.getFinalFileUrl(path), bytes.NewReader(data)) if err != nil { return err } req.SetBasicAuth(s.webDavConfig.Username, s.webDavConfig.Password) resp, err := s.httpClient.Do(req) if err != nil { log.Errorf(ctx, "[webdav_storage.Save] cannot save file, because %s", err.Error()) return err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { log.Errorf(ctx, "[webdav_storage.Save] cannot read response (http status code %d) body, because %s", resp.StatusCode, err.Error()) return err } if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { log.Errorf(ctx, "[webdav_storage.Save] cannot save file, http status code is %d, response is %s", resp.StatusCode, string(body)) return errs.ErrSystemError } return nil } // Delete returns whether delete the object according to specified the file path successfully func (s *WebDAVObjectStorage) Delete(ctx core.Context, path string) error { req, err := http.NewRequest("DELETE", s.getFinalFileUrl(path), nil) if err != nil { return err } req.SetBasicAuth(s.webDavConfig.Username, s.webDavConfig.Password) resp, err := s.httpClient.Do(req) if err != nil { log.Errorf(ctx, "[webdav_storage.Delete] cannot delete file, because %s", err.Error()) return err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { log.Errorf(ctx, "[webdav_storage.Delete] cannot read response (http status code %d) body, because %s", resp.StatusCode, err.Error()) return err } if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusNotFound { log.Errorf(ctx, "[webdav_storage.Delete] cannot delete file, http status code is %d, response is %s", resp.StatusCode, string(body)) return errs.ErrSystemError } return nil } func (s *WebDAVObjectStorage) directoryExists(ctx core.Context, path string) (bool, error) { req, err := http.NewRequest("PROPFIND", s.getFinalDirectoryUrl(path), nil) if err != nil { return false, err } req.SetBasicAuth(s.webDavConfig.Username, s.webDavConfig.Password) resp, err := s.httpClient.Do(req) if err != nil { log.Errorf(ctx, "[webdav_storage.directoryExists] cannot check directory exists, because %s", err.Error()) return false, err } if resp.StatusCode == http.StatusMultiStatus || resp.StatusCode == http.StatusOK { return true, nil } else if resp.StatusCode == http.StatusNotFound { return false, nil } log.Errorf(ctx, "[webdav_storage.directoryExists] cannot check directory exists, http status code is %d", resp.StatusCode) return false, errs.ErrSystemError } func (s *WebDAVObjectStorage) createDirectory(ctx core.Context, path string) error { req, err := http.NewRequest("MKCOL", s.getFinalDirectoryUrl(path), nil) if err != nil { return err } req.SetBasicAuth(s.webDavConfig.Username, s.webDavConfig.Password) resp, err := s.httpClient.Do(req) if err != nil { log.Errorf(ctx, "[webdav_storage.createDirectory] cannot create directory, because %s", err.Error()) return err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { log.Errorf(ctx, "[webdav_storage.createDirectory] cannot read response (http status code %d) body, because %s", resp.StatusCode, err.Error()) return err } if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusMethodNotAllowed { log.Errorf(ctx, "[webdav_storage.createDirectory] cannot create directory, http status code is %d, response is %s", resp.StatusCode, string(body)) return errs.ErrSystemError } return nil } func (s *WebDAVObjectStorage) createAllDirectories(ctx core.Context, currentPath string, path string) error { directories := strings.Split(path, "/") for _, dir := range directories { if len(dir) == 0 { continue } currentPath = currentPath + "/" + dir exists, err := s.directoryExists(ctx, currentPath) if err != nil { return err } if !exists { err = s.createDirectory(ctx, currentPath) if err != nil { return err } } } return nil } func (s *WebDAVObjectStorage) getFinalFileUrl(filePath string) string { finalUrl := s.webDavConfig.Url if len(finalUrl) < 1 || finalUrl[len(finalUrl)-1] != '/' { finalUrl = finalUrl + "/" } finalPath := s.getFinalPath(filePath) if len(finalPath) > 0 && finalPath[0] == '/' { finalPath = finalPath[1:] } return finalUrl + finalPath } func (s *WebDAVObjectStorage) getFinalDirectoryUrl(dirPath string) string { finalUrl := s.webDavConfig.Url if len(finalUrl) < 1 || finalUrl[len(finalUrl)-1] != '/' { finalUrl = finalUrl + "/" } if len(dirPath) > 0 && dirPath[0] == '/' { dirPath = dirPath[1:] } if len(dirPath) > 0 && dirPath[len(dirPath)-1] != '/' { dirPath = dirPath + "/" } return finalUrl + dirPath } func (s *WebDAVObjectStorage) getFinalPath(path string) string { rootPath := s.rootPath if len(rootPath) < 1 || rootPath[len(rootPath)-1] != '/' { rootPath = rootPath + "/" } if len(path) > 0 && path[0] == '/' { path = path[1:] } path = strings.ReplaceAll(path, "\\", "/") return rootPath + path }