From 57b740a490bd997cd0ff086311d54f210dc4b673 Mon Sep 17 00:00:00 2001 From: "Gustavo L de Mello (Guz)" Date: Wed, 8 Jan 2025 11:15:19 -0300 Subject: [PATCH] feat(blogo,gitea): fs.FS implementation for repositoryFS --- blogo/gitea/fs.go | 240 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 240 insertions(+) diff --git a/blogo/gitea/fs.go b/blogo/gitea/fs.go index 1f4940c..7011867 100644 --- a/blogo/gitea/fs.go +++ b/blogo/gitea/fs.go @@ -15,6 +15,20 @@ package gitea +import ( + "bytes" + "encoding/base64" + "errors" + "io" + "io/fs" + "net/http" + "os" + "path" + "slices" + "syscall" + "time" +) + type repositoryFS struct { owner string repo string @@ -27,3 +41,229 @@ func newRepositoryFS(owner, repo, ref string, client *client) *repositoryFS { return &repositoryFS{owner: owner, repo: repo, ref: ref, client: client} } +func (fsys *repositoryFS) Open(name string) (fs.File, error) { + if !fs.ValidPath(name) { + return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid} + } + + file, _, err := fsys.client.GetContents(fsys.owner, fsys.repo, fsys.ref, name) + if err == nil { + return &repositoryFile{ + contentsResponse: *file, + + owner: fsys.owner, + repo: fsys.repo, + ref: fsys.ref, + client: fsys.client, + + contents: nil, + }, nil + } + + // If previous call returned a error, it may be because the file is a directory, + // so we will call from it's parent directory to be able to get it's metadata. + list, res, err := fsys.client.ListContents(fsys.owner, fsys.repo, fsys.ref, path.Dir(name)) + if err != nil { + if res.StatusCode == http.StatusUnauthorized { + return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrPermission} + } else if res.StatusCode == http.StatusNotFound { + return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist} + } + return nil, &fs.PathError{Op: "open", Path: name, Err: err} + } + + i := slices.IndexFunc(list, func(i *contentsResponse) bool { + return i.Path == name + }) + if i == -1 { + return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist} + } + + dir := list[i] + if dir.Type != "dir" { + return nil, &fs.PathError{ + Op: "open", + Path: name, + Err: errors.New("unexpected, directory found is not of type 'dir'"), + } + } + + f := &repositoryFile{ + contentsResponse: *dir, + + owner: fsys.owner, + repo: fsys.repo, + ref: fsys.ref, + client: fsys.client, + + contents: nil, + } + + return &repositoryDirFile{*f, 0}, nil +} + +// Implements fs.File to represent a remote file in the repository. The contents of +// the file are filled on the first Read call, reusing the base64-encoded +// *contentsResponse.Content if available, if not, the file calls the API to retrieve +// the raw contents. +// +// To prevent possible content changes after this object has been initialized, if none +// ref is provided, it uses the *contentsResponse.LastCommitSha as a ref. +type repositoryFile struct { + contentsResponse + + owner string + repo string + ref string + + client *client + + contents io.ReadCloser +} + +func (f *repositoryFile) Stat() (fs.FileInfo, error) { + return &repositoryFileInfo{*f}, nil +} + +func (f *repositoryFile) Read(p []byte) (int, error) { + var err error + + if f.contents == nil && f.Type == "file" { + f.contents, err = f.getFileContents() + } + + if err != nil { + return 0, errors.Join(errors.New("failed to fetch file contents from API"), err) + } + + return f.contents.Read(p) +} + +func (f *repositoryFile) Close() error { + return f.contents.Close() +} + +func (f *repositoryFile) getFileContents() (io.ReadCloser, error) { + if *f.Content != "" && f.Encoding != nil && *f.Encoding == "base64" { + b, err := base64.StdEncoding.DecodeString(*f.Content) + if err == nil { + return io.NopCloser(bytes.NewReader(b)), nil + } + } + + ref := f.ref + if ref == "" { + ref = f.contentsResponse.LastCommitSha + } + + r, _, err := f.client.GetFileReader(f.owner, f.repo, ref, f.Path, true) + return r, err +} + +// Implements fs.ReadDirFile for the underlying 'repositoryFile'. +// 'repositoryFile' should be of type "dir", and not a list of said directory +// content. +type repositoryDirFile struct { + repositoryFile + n int +} + +func (f *repositoryDirFile) Read(p []byte) (int, error) { + return 0, nil +} + +func (f *repositoryDirFile) Close() error { + return nil +} + +func (f *repositoryDirFile) ReadDir(n int) ([]fs.DirEntry, error) { + list, _, err := f.client.ListContents(f.owner, f.repo, f.ref, f.Path) + if err != nil { + return []fs.DirEntry{}, err + } + + start, end := f.n, f.n+n+1 + + if end > len(list)-1 { + end = len(list) + 1 + err = io.EOF + } + + list = list[start:end] + entries := make([]fs.DirEntry, len(list)) + for i, v := range list { + entries[i] = &repositoryDirEntry{repositoryFile{ + contentsResponse: *v, + + owner: f.owner, + repo: f.repo, + ref: f.ref, + client: f.client, + }} + } + + return entries, err +} + +// Implements fs.DirEntry for the embedded 'repositoryFile' +type repositoryDirEntry struct { + repositoryFile +} + +func (e *repositoryDirEntry) Name() string { + i, _ := e.Info() + return i.Name() +} + +func (e *repositoryDirEntry) IsDir() bool { + i, _ := e.Info() + return i.IsDir() +} + +func (e *repositoryDirEntry) Type() fs.FileMode { + i, _ := e.Info() + return i.Mode().Type() +} + +func (e *repositoryDirEntry) Info() (fs.FileInfo, error) { + return &repositoryFileInfo{e.repositoryFile}, nil +} + +// Implements fs.FileInfo, getting information from the embedded 'repositoryFile' +type repositoryFileInfo struct { + repositoryFile +} + +func (fi *repositoryFileInfo) Name() string { + return fi.contentsResponse.Name +} + +func (fi *repositoryFileInfo) Size() int64 { + return fi.contentsResponse.Size +} + +func (fi *repositoryFileInfo) Mode() fs.FileMode { + if fi.Type == "symlink" { + return os.FileMode(fs.ModeSymlink | syscall.S_IRUSR | syscall.S_IRGRP | syscall.S_IROTH) + } else if fi.IsDir() { + return os.FileMode(fs.ModeDir | syscall.S_IRUSR | syscall.S_IRGRP | syscall.S_IROTH) + } + return os.FileMode(syscall.S_IRUSR | syscall.S_IRGRP | syscall.S_IROTH) +} + +func (fi *repositoryFileInfo) ModTime() time.Time { + commit, _, err := fi.client.GetSingleCommit(fi.owner, fi.repo, fi.LastCommitSha) + if err != nil { + return time.Time{} + } + + return commit.Created +} + +func (fi *repositoryFileInfo) IsDir() bool { + return fi.Type == "dir" +} + +func (fi *repositoryFileInfo) Sys() any { + return nil +}