Files
x/blogo/gitea/fs.go

275 lines
6.4 KiB
Go

// Copyright 2025-present Gustavo "Guz" L. de Mello
// Copyright 2025-present The Lored.dev Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gitea
import (
"bytes"
"encoding/base64"
"errors"
"io"
"io/fs"
"net/http"
"os"
"path"
"slices"
"syscall"
"time"
)
type repositoryFS struct {
owner string
repo string
ref string
client *client
}
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.
path := path.Dir(name)
if path == "." {
path = ""
}
list, res, err := fsys.client.ListContents(fsys.owner, fsys.repo, fsys.ref, path)
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
}