package httpfs import ( "bytes" "errors" "fmt" "io" "io/ioutil" "net/http" "net/http/httptest" "net/url" "strconv" "time" "gitlab.com/golang-utils/fs" "gitlab.com/golang-utils/fs/mapfs" "gitlab.com/golang-utils/fs/path" ) /* The idea is to make a remote filesystem based on the http methods get, put, post and delete this library would mainly consist of a client (the FS), but for testing purposes we'll also implement a tiny server based on a mapfs for testing purposes we would also */ var _ fs.Remote = &FS{} type TestHandler struct { Error error store *mapfs.FS } func NewTestHandler(rem *path.Remote) (*TestHandler, error) { st, err := mapfs.New(rem) if err != nil { return nil, err } return &TestHandler{ store: st, }, nil } var _ http.Handler = &TestHandler{} func (serv *TestHandler) serveGet(wr http.ResponseWriter, req *http.Request) { p := path.Relative(req.URL.Path[1:]) defer req.Body.Close() rd, err := serv.store.Reader(p) if err != nil { switch { case errors.Is(err, fs.ErrNotFound): wr.WriteHeader(http.StatusNotFound) default: wr.WriteHeader(http.StatusBadRequest) } return } bt, err := ioutil.ReadAll(rd) defer rd.Close() if err != nil { wr.WriteHeader(http.StatusBadRequest) return } headers, err := serv.store.GetMeta(p) if err != nil { wr.WriteHeader(http.StatusBadRequest) return } if headers == nil { headers = map[string][]byte{} } _, has := headers["Content-Type"] if !has { headers["Content-Type"] = []byte("application/octet-stream") } for k, v := range headers { wr.Header().Set(k, string(v)) } wr.Write(bt) } func (serv *TestHandler) servePut(wr http.ResponseWriter, req *http.Request) { p := path.Relative(req.URL.Path[1:]) defer req.Body.Close() var meta = map[string][]byte{} for k := range req.Header { meta[k] = []byte(req.Header.Get(k)) } err := serv.store.WriteWithMeta(p, req.Body, meta, true) if err != nil { switch { case errors.Is(err, fs.ErrNotFound): wr.WriteHeader(http.StatusNotFound) default: wr.WriteHeader(http.StatusBadRequest) } return } wr.WriteHeader(http.StatusOK) } func (serv *TestHandler) serveDelete(wr http.ResponseWriter, req *http.Request) { p := path.Relative(req.URL.Path[1:]) defer req.Body.Close() serv.store.Delete(p, true) wr.WriteHeader(http.StatusOK) } func (serv *TestHandler) ServeHTTP(wr http.ResponseWriter, req *http.Request) { if len(req.URL.Path) < 2 { wr.WriteHeader(http.StatusNotFound) return } switch req.Method { case "GET": serv.serveGet(wr, req) case "PUT": serv.servePut(wr, req) case "DELETE": serv.serveDelete(wr, req) default: wr.WriteHeader(http.StatusNotImplemented) } } type FS struct { root url.URL TestHandler http.Handler Timeout time.Duration //*dirtreefs.FS } func New(rem *path.Remote) (*FS, error) { if !path.IsDir(rem) { return nil, fs.ErrExpectedDir.Params(rem) } u := rem.URL if u == nil { return nil, fmt.Errorf("url expected %s", rem) } m := &FS{ root: *u, Timeout: 2 * time.Second, } return m, nil } func (f *FS) head(rel path.Relative) (headers map[string][]byte, err error) { resp, err := f.request("HEAD", rel, nil, nil) //c := http.Client{Timeout: f.timeout} //resp, err := c.Head(f.Abs(rel).String()) if err != nil { switch err { case http.ErrMissingFile: return nil, fs.ErrNotFound.Params(rel) default: return nil, err } } defer resp.Body.Close() headers = map[string][]byte{} for k := range resp.Header { headers[k] = []byte(resp.Header.Get(k)) } return headers, nil //bt, err := io.ReadAll(resp.Body) //fmt.Println(resp.Header) } // TODO: authentication func (f *FS) mkrequestStr(relpath path.Relative) string { str := f.root.JoinPath(relpath.String()).String() if path.IsDir(relpath) { str += "/" } return path.MustRemote(str).String() //return f.Abs(relpath).String() //return f.requestURL.ResolveReference(relpath).String() //return f.requestURL.ResolveReference(relpath).String() //path.MustRemote(). /* if relpath.String() == "" { return f.requestURL.String() } return f.requestURL.JoinPath() .Join(relpath.String()) */ } func (f *FS) doRequest(req *http.Request) (resp *http.Response, err error) { if f.TestHandler != nil { rec := httptest.NewRecorder() f.TestHandler.ServeHTTP(rec, req) return rec.Result(), nil } c := http.Client{Timeout: f.Timeout} return c.Do(req) } func (f *FS) request(method string, relpath path.Relative, body io.ReadCloser, headers map[string][]byte) (resp *http.Response, err error) { switch method { case "GET", "POST", "PUT", "DELETE", "HEAD": default: return nil, fmt.Errorf("unknown HTTP method: %s", method) } //c := http.Client{Timeout: f.Timeout} req, err := http.NewRequest(method, f.mkrequestStr(relpath), body) if err != nil { fmt.Printf("error %s", err) return } if len(headers) > 0 { for k, v := range headers { req.Header.Add(k, string(v)) } } //return c.Do(req) return f.doRequest(req) } /* For a PUT request: HTTP 200, HTTP 204 should imply "resource updated successfully". HTTP 201 if the PUT request created a new resource. For a DELETE request: HTTP 200 or HTTP 204 should imply "resource deleted successfully". */ func (f *FS) put(rel path.Relative, data io.ReadCloser, headers map[string][]byte) error { //c := http.Client{Timeout: f.timeout} if headers == nil { headers = map[string][]byte{} } _, has := headers["Content-Type"] if !has { headers["Content-Type"] = []byte("application/octet-stream") } resp, err := f.request("PUT", rel, data, headers) //c. //resp, err := c.Post(f.Abs(rel).String(), contenttype, data) defer resp.Body.Close() defer data.Close() if err != nil { switch err { case http.ErrMissingFile: return fs.ErrNotFound.Params(rel) default: return err } } switch resp.StatusCode { case http.StatusOK: case http.StatusCreated: case 204: default: return fs.ErrNotFound.Params(rel) } return nil /* myJson := bytes.NewBuffer([]byte(`{"name":"Maximilien"}`)) resp, err := c.Post("https://www.google.com", "application/json", myJson) if err != nil { fmt.Errorf("Error %s", err) return } */ } /* req.Header.Add("Authorization", fmt.Sprintf("token %s", os.Getenv("TOKEN")) */ func (f *FS) get(rel path.Relative) (io.ReadCloser, error) { resp, err := f.request("GET", rel, nil, nil) if err != nil { switch err { case http.ErrMissingFile: return nil, fs.ErrNotFound.Params(rel) default: return nil, err } } if resp.StatusCode != http.StatusOK { return nil, fs.ErrNotFound.Params(rel) } if path.IsDir(rel) { defer resp.Body.Close() bt, err := io.ReadAll(resp.Body) if err != nil { return fs.ReadCloser(bytes.NewReader(bt)), nil } } //defer resp.Body.Close() //body, err := ioutil.ReadAll(resp.Body) //fmt.Printf("Body : %s", body) return resp.Body, nil } func (m *FS) Delete(p path.Relative, recursive bool) error { resp, err := m.request("DELETE", p, nil, nil) defer resp.Body.Close() switch err { case http.ErrMissingFile: return fs.ErrNotFound.Params(p) default: return err } } func (m *FS) Abs(p path.Relative) path.Absolute { str := m.root.JoinPath(p.String()).String() if path.IsDir(p) { str += "/" } return path.MustRemote(str) } func (m *FS) Exists(p path.Relative) bool { _, err := m.head(p) return err == nil } func (m *FS) ModTime(p path.Relative) (t time.Time, err error) { headers, err := m.head(p) if err != nil { return t, err } return http.ParseTime(string(headers["Date"])) } // we don't need to write all directories, since they are just implicit // func (m *MapFS) Write(p path.Relative, rd io.ReadCloser, recursive bool) (err error) { func (m *FS) write(p path.Relative, rd io.ReadCloser, meta map[string][]byte, recursive bool) (err error) { return m.put(p, rd, meta) /* if path.IsDir(p) && recursive { return m.Mkdirall(p) } if recursive { m.Mkdirall(path.Dir(p).Relative()) } */ /* var bt []byte _ = bt if rd != nil { bt, err = io.ReadAll(rd) rd.Close() if err != nil { return err } } //m.add(p, bt, perm, meta) return nil */ } func (m *FS) Reader(p path.Relative) (io.ReadCloser, error) { return m.get(p) /* var bf bytes.Buffer _ = bf if path.IsDir(p) { // find all subdirs and files pstr := p.String() _ = pstr */ /* for k := range m.Files { kstr := k.String() if kstr == pstr { continue } if strings.HasPrefix(kstr, pstr) { rel, err := filepath.Rel(pstr, kstr) if err == nil { if strings.Contains(rel, "/") { continue } bf.WriteString(rel + "\n") } continue } } return fs.ReadCloser(strings.NewReader(bf.String())), nil */ //} /* f, has := m.Files[p] if !has { return nil, fs.ErrNotFound } */ //return fs.ReadCloser(bytes.NewReader(nil)), nil } func (m *FS) Write(p path.Relative, rd io.ReadCloser, recursive bool) (err error) { return m.WriteWithMeta(p, rd, nil, recursive) } func (m *FS) WriteWithMeta(p path.Relative, rd io.ReadCloser, meta map[string][]byte, recursive bool) error { return m.write(p, rd, meta, recursive) } func (d *FS) Size(name path.Relative) int64 { if path.IsDir(name) { return 0 } headers, err := d.head(name) if err != nil { return -1 } l, has := headers["Content-Length"] if !has { return -1 } i, err := strconv.Atoi(string(l)) if err != nil { return -1 } return int64(i) /* f, has := d.Files[name] if !has { return -1 } */ //return int64(len(f.Data)) //return 0 } func (f *FS) GetMeta(rel path.Relative) (map[string][]byte, error) { return f.head(rel) //return nil, fs.ErrNotFound } func (f *FS) SetMeta(p path.Relative, meta map[string][]byte) error { return fs.ErrNotSupported.Params("SetMeta", f) //return fs.ErrNotFound } func (f *FS) HostName() string { return f.root.Host } func (f *FS) Password() string { u := f.root.User if u == nil { return "" } pw, ok := u.Password() if !ok { return "" } return pw } func (f *FS) Port() int { port := f.root.Port() if f.root.Port() == "" { return 0 } po, err := strconv.Atoi(port) if err != nil { return 0 } return po } func (f *FS) UserName() string { if f.root.User == nil { return "" } return f.root.User.Username() } func (f *FS) Scheme() string { return f.root.Scheme }