feat(blogo,gitea): fs.FS implementation for repositoryFS
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user