refactor torrent
Some checks failed
docker / build-docker (linux/amd64) (push) Successful in 1m33s
docker / build-docker (linux/386) (push) Successful in 1m39s
docker / build-docker (linux/arm64) (push) Successful in 7m6s
docker / build-docker (linux/arm/v7) (push) Successful in 7m31s
docker / build-docker (linux/arm64/v8) (push) Has been cancelled
Some checks failed
docker / build-docker (linux/amd64) (push) Successful in 1m33s
docker / build-docker (linux/386) (push) Successful in 1m39s
docker / build-docker (linux/arm64) (push) Successful in 7m6s
docker / build-docker (linux/arm/v7) (push) Successful in 7m31s
docker / build-docker (linux/arm64/v8) (push) Has been cancelled
This commit is contained in:
parent
99cdd5471e
commit
991c15fdef
33 changed files with 223 additions and 275 deletions
|
@ -33,7 +33,7 @@ models:
|
|||
resolver: true
|
||||
extraFields:
|
||||
T:
|
||||
type: "*git.kmsign.ru/royalcat/tstor/src/host/controller.Torrent"
|
||||
type: "*git.kmsign.ru/royalcat/tstor/src/host/torrent.Controller"
|
||||
TorrentFile:
|
||||
extraFields:
|
||||
F:
|
||||
|
@ -57,7 +57,7 @@ models:
|
|||
resolver: true
|
||||
extraFields:
|
||||
FS:
|
||||
type: "*git.kmsign.ru/royalcat/tstor/src/host/vfs.TorrentFS"
|
||||
type: "*git.kmsign.ru/royalcat/tstor/src/host/torrent.TorrentFS"
|
||||
ResolverFS:
|
||||
fields:
|
||||
entries:
|
||||
|
|
|
@ -19,7 +19,7 @@ import (
|
|||
"git.kmsign.ru/royalcat/tstor/src/config"
|
||||
"git.kmsign.ru/royalcat/tstor/src/delivery"
|
||||
"git.kmsign.ru/royalcat/tstor/src/host"
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/service"
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/torrent"
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/vfs"
|
||||
"git.kmsign.ru/royalcat/tstor/src/telemetry"
|
||||
"github.com/go-git/go-billy/v5/osfs"
|
||||
|
@ -90,12 +90,12 @@ func run(configPath string) error {
|
|||
}
|
||||
|
||||
sourceFs := osfs.New(conf.SourceDir, osfs.WithBoundOS())
|
||||
srv, err := service.New(sourceFs, conf.TorrentClient)
|
||||
srv, err := torrent.NewService(sourceFs, conf.TorrentClient)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating service: %w", err)
|
||||
}
|
||||
|
||||
sfs := host.NewTorrentStorage(
|
||||
sfs := host.NewHostedFS(
|
||||
vfs.NewCtxBillyFs("/", ctxbilly.WrapFileSystem(sourceFs)),
|
||||
srv,
|
||||
)
|
||||
|
@ -174,7 +174,7 @@ func run(configPath string) error {
|
|||
go func() {
|
||||
logFilename := filepath.Join(conf.Log.Path, "logs")
|
||||
|
||||
err := delivery.New(nil, service.NewStats(), srv, sfs, logFilename, conf)
|
||||
err := delivery.New(nil, srv, sfs, logFilename, conf)
|
||||
if err != nil {
|
||||
log.Error(ctx, "error initializing HTTP server", rlog.Error(err))
|
||||
}
|
||||
|
|
|
@ -7,12 +7,12 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/service"
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/torrent"
|
||||
"github.com/anacrolix/missinggo/v2/filecache"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var apiStatusHandler = func(fc *filecache.Cache, ss *service.Stats) gin.HandlerFunc {
|
||||
var apiStatusHandler = func(fc *filecache.Cache, ss *torrent.Stats) gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
stat := gin.H{
|
||||
"torrentStats": ss.GlobalStats(),
|
||||
|
|
|
@ -3,6 +3,7 @@ package model
|
|||
import (
|
||||
"context"
|
||||
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/torrent"
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/vfs"
|
||||
)
|
||||
|
||||
|
@ -26,8 +27,8 @@ func FillFsEntry(ctx context.Context, e FsElem, fs vfs.Filesystem, path string)
|
|||
Name: e.Name(),
|
||||
FS: e,
|
||||
}, nil
|
||||
case *vfs.TorrentFS:
|
||||
e := e.(*vfs.TorrentFS)
|
||||
case *torrent.TorrentFS:
|
||||
e := e.(*torrent.TorrentFS)
|
||||
torrent, err := MapTorrent(ctx, e.Torrent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -3,39 +3,39 @@ package model
|
|||
import (
|
||||
"context"
|
||||
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/controller"
|
||||
"github.com/anacrolix/torrent"
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/torrent"
|
||||
atorrent "github.com/anacrolix/torrent"
|
||||
)
|
||||
|
||||
func MapPeerSource(source torrent.PeerSource) string {
|
||||
func MapPeerSource(source atorrent.PeerSource) string {
|
||||
switch source {
|
||||
case torrent.PeerSourceDirect:
|
||||
case atorrent.PeerSourceDirect:
|
||||
return "Direct"
|
||||
case torrent.PeerSourceUtHolepunch:
|
||||
case atorrent.PeerSourceUtHolepunch:
|
||||
return "Ut Holepunch"
|
||||
case torrent.PeerSourceDhtAnnouncePeer:
|
||||
case atorrent.PeerSourceDhtAnnouncePeer:
|
||||
return "DHT Announce"
|
||||
case torrent.PeerSourceDhtGetPeers:
|
||||
case atorrent.PeerSourceDhtGetPeers:
|
||||
return "DHT"
|
||||
case torrent.PeerSourceIncoming:
|
||||
case atorrent.PeerSourceIncoming:
|
||||
return "Incoming"
|
||||
case torrent.PeerSourceTracker:
|
||||
case atorrent.PeerSourceTracker:
|
||||
return "Tracker"
|
||||
case torrent.PeerSourcePex:
|
||||
case atorrent.PeerSourcePex:
|
||||
return "PEX"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func MapTorrent(ctx context.Context, t *controller.Torrent) (*Torrent, error) {
|
||||
func MapTorrent(ctx context.Context, t *torrent.Controller) (*Torrent, error) {
|
||||
downloading := false
|
||||
files, err := t.Files(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, file := range files {
|
||||
if file.Priority() > torrent.PiecePriorityNone && file.BytesCompleted() < file.Length() {
|
||||
if file.Priority() > atorrent.PiecePriorityNone && file.BytesCompleted() < file.Length() {
|
||||
downloading = true
|
||||
break
|
||||
}
|
||||
|
|
|
@ -5,9 +5,9 @@ package model
|
|||
import (
|
||||
"time"
|
||||
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/controller"
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/torrent"
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/vfs"
|
||||
"github.com/anacrolix/torrent"
|
||||
torrent1 "github.com/anacrolix/torrent"
|
||||
)
|
||||
|
||||
type Dir interface {
|
||||
|
@ -180,14 +180,14 @@ type Torrent struct {
|
|||
ExcludedFiles []*TorrentFile `json:"excludedFiles"`
|
||||
Peers []*TorrentPeer `json:"peers"`
|
||||
Downloading bool `json:"downloading"`
|
||||
T *controller.Torrent `json:"-"`
|
||||
T *torrent.Controller `json:"-"`
|
||||
}
|
||||
|
||||
type TorrentFs struct {
|
||||
Name string `json:"name"`
|
||||
Torrent *Torrent `json:"torrent"`
|
||||
Entries []FsEntry `json:"entries"`
|
||||
FS *vfs.TorrentFS `json:"-"`
|
||||
Name string `json:"name"`
|
||||
Torrent *Torrent `json:"torrent"`
|
||||
Entries []FsEntry `json:"entries"`
|
||||
FS *torrent.TorrentFS `json:"-"`
|
||||
}
|
||||
|
||||
func (TorrentFs) IsDir() {}
|
||||
|
@ -206,10 +206,10 @@ func (this TorrentFs) GetEntries() []FsEntry {
|
|||
func (TorrentFs) IsFsEntry() {}
|
||||
|
||||
type TorrentFile struct {
|
||||
Filename string `json:"filename"`
|
||||
Size int64 `json:"size"`
|
||||
BytesCompleted int64 `json:"bytesCompleted"`
|
||||
F *torrent.File `json:"-"`
|
||||
Filename string `json:"filename"`
|
||||
Size int64 `json:"size"`
|
||||
BytesCompleted int64 `json:"bytesCompleted"`
|
||||
F *torrent1.File `json:"-"`
|
||||
}
|
||||
|
||||
type TorrentFileEntry struct {
|
||||
|
@ -230,12 +230,12 @@ type TorrentFilter struct {
|
|||
}
|
||||
|
||||
type TorrentPeer struct {
|
||||
IP string `json:"ip"`
|
||||
DownloadRate float64 `json:"downloadRate"`
|
||||
Discovery string `json:"discovery"`
|
||||
Port int64 `json:"port"`
|
||||
ClientName string `json:"clientName"`
|
||||
F *torrent.PeerConn `json:"-"`
|
||||
IP string `json:"ip"`
|
||||
DownloadRate float64 `json:"downloadRate"`
|
||||
Discovery string `json:"discovery"`
|
||||
Port int64 `json:"port"`
|
||||
ClientName string `json:"clientName"`
|
||||
F *torrent1.PeerConn `json:"-"`
|
||||
}
|
||||
|
||||
type TorrentProgress struct {
|
||||
|
|
|
@ -14,7 +14,7 @@ import (
|
|||
"git.kmsign.ru/royalcat/tstor/pkg/uuid"
|
||||
graph "git.kmsign.ru/royalcat/tstor/src/delivery/graphql"
|
||||
"git.kmsign.ru/royalcat/tstor/src/delivery/graphql/model"
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/service"
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/torrent"
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
aih "github.com/anacrolix/torrent/types/infohash"
|
||||
)
|
||||
|
@ -79,7 +79,7 @@ func (r *mutationResolver) DownloadTorrent(ctx context.Context, infohash string,
|
|||
f = *file
|
||||
}
|
||||
|
||||
err := r.Service.Download(ctx, &service.TorrentDownloadTask{
|
||||
err := r.Service.Download(ctx, &torrent.DownloadTask{
|
||||
ID: uuid.New(),
|
||||
InfoHash: aih.FromHexString(infohash),
|
||||
File: f,
|
||||
|
|
|
@ -11,7 +11,7 @@ import (
|
|||
|
||||
graph "git.kmsign.ru/royalcat/tstor/src/delivery/graphql"
|
||||
"git.kmsign.ru/royalcat/tstor/src/delivery/graphql/model"
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/controller"
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/torrent"
|
||||
)
|
||||
|
||||
// Torrents is the resolver for the torrents field.
|
||||
|
@ -80,7 +80,7 @@ func (r *queryResolver) Torrents(ctx context.Context, filter *model.TorrentsFilt
|
|||
tr = append(tr, d)
|
||||
}
|
||||
|
||||
slices.SortStableFunc(torrents, func(t1, t2 *controller.Torrent) int {
|
||||
slices.SortStableFunc(torrents, func(t1, t2 *torrent.Controller) int {
|
||||
return strings.Compare(t1.Name(), t2.Name())
|
||||
})
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package resolver
|
||||
|
||||
import (
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/service"
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/torrent"
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/vfs"
|
||||
"github.com/go-git/go-billy/v5"
|
||||
)
|
||||
|
@ -11,7 +11,7 @@ import (
|
|||
// It serves as dependency injection for your app, add any dependencies you require here.
|
||||
|
||||
type Resolver struct {
|
||||
Service *service.Service
|
||||
Service *torrent.Service
|
||||
VFS vfs.Filesystem
|
||||
SourceFS billy.Filesystem
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
|
||||
"git.kmsign.ru/royalcat/tstor/pkg/rlog"
|
||||
"git.kmsign.ru/royalcat/tstor/src/config"
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/service"
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/torrent"
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/vfs"
|
||||
"github.com/anacrolix/missinggo/v2/filecache"
|
||||
echopprof "github.com/labstack/echo-contrib/pprof"
|
||||
|
@ -15,7 +15,7 @@ import (
|
|||
"github.com/labstack/echo/v4/middleware"
|
||||
)
|
||||
|
||||
func New(fc *filecache.Cache, ss *service.Stats, s *service.Service, vfs vfs.Filesystem, logPath string, cfg *config.Settings) error {
|
||||
func New(fc *filecache.Cache, s *torrent.Service, vfs vfs.Filesystem, logPath string, cfg *config.Settings) error {
|
||||
log := slog.With()
|
||||
|
||||
r := echo.New()
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
"git.kmsign.ru/royalcat/tstor/pkg/rlog"
|
||||
graph "git.kmsign.ru/royalcat/tstor/src/delivery/graphql"
|
||||
"git.kmsign.ru/royalcat/tstor/src/delivery/graphql/resolver"
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/service"
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/torrent"
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/vfs"
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
"github.com/99designs/gqlgen/graphql/handler"
|
||||
|
@ -18,7 +18,7 @@ import (
|
|||
"github.com/ravilushqa/otelgqlgen"
|
||||
)
|
||||
|
||||
func GraphQLHandler(service *service.Service, vfs vfs.Filesystem) http.Handler {
|
||||
func GraphQLHandler(service *torrent.Service, vfs vfs.Filesystem) http.Handler {
|
||||
graphqlHandler := handler.NewDefaultServer(
|
||||
graph.NewExecutableSchema(
|
||||
graph.Config{
|
||||
|
|
|
@ -97,7 +97,7 @@ func newFile(ctx context.Context, name string, f vfs.File, df func() ([]os.FileI
|
|||
return &webDAVFile{
|
||||
ctx: ctx,
|
||||
f: f,
|
||||
fi: newFileInfo(name, f.Size(), f.IsDir()),
|
||||
fi: NewFileInfo(name, f.Size(), f.IsDir()),
|
||||
dirFunc: df,
|
||||
}
|
||||
}
|
||||
|
@ -185,7 +185,7 @@ type webDAVFileInfo struct {
|
|||
isDir bool
|
||||
}
|
||||
|
||||
func newFileInfo(name string, size int64, isDir bool) *webDAVFileInfo {
|
||||
func NewFileInfo(name string, size int64, isDir bool) *webDAVFileInfo {
|
||||
return &webDAVFileInfo{
|
||||
name: name,
|
||||
size: size,
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
package host
|
||||
|
||||
import (
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/service"
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/torrent"
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/vfs"
|
||||
)
|
||||
|
||||
func NewTorrentStorage(sourceFS vfs.Filesystem, tsrv *service.Service) vfs.Filesystem {
|
||||
func NewHostedFS(sourceFS vfs.Filesystem, tsrv *torrent.Service) vfs.Filesystem {
|
||||
factories := map[string]vfs.FsFactory{
|
||||
".torrent": tsrv.NewTorrentFs,
|
||||
}
|
||||
|
|
|
@ -1,33 +1,32 @@
|
|||
package controller
|
||||
package torrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/store"
|
||||
"github.com/anacrolix/torrent"
|
||||
)
|
||||
|
||||
type Torrent struct {
|
||||
type Controller struct {
|
||||
torrentFilePath string
|
||||
t *torrent.Torrent
|
||||
rep *store.FilesMappings
|
||||
rep *filesMappingsStore
|
||||
}
|
||||
|
||||
func NewTorrent(t *torrent.Torrent, rep *store.FilesMappings) *Torrent {
|
||||
return &Torrent{t: t, rep: rep}
|
||||
func newController(t *torrent.Torrent, rep *filesMappingsStore) *Controller {
|
||||
return &Controller{t: t, rep: rep}
|
||||
}
|
||||
|
||||
func (s *Torrent) TorrentFilePath() string {
|
||||
func (s *Controller) TorrentFilePath() string {
|
||||
return s.torrentFilePath
|
||||
}
|
||||
|
||||
func (s *Torrent) Torrent() *torrent.Torrent {
|
||||
func (s *Controller) Torrent() *torrent.Torrent {
|
||||
return s.t
|
||||
}
|
||||
|
||||
func (c *Torrent) Name() string {
|
||||
func (c *Controller) Name() string {
|
||||
<-c.t.GotInfo()
|
||||
if name := c.t.Name(); name != "" {
|
||||
return name
|
||||
|
@ -36,27 +35,27 @@ func (c *Torrent) Name() string {
|
|||
return c.InfoHash()
|
||||
}
|
||||
|
||||
func (s *Torrent) InfoHash() string {
|
||||
func (s *Controller) InfoHash() string {
|
||||
<-s.t.GotInfo()
|
||||
return s.t.InfoHash().HexString()
|
||||
}
|
||||
|
||||
func (s *Torrent) BytesCompleted() int64 {
|
||||
func (s *Controller) BytesCompleted() int64 {
|
||||
<-s.t.GotInfo()
|
||||
return s.t.BytesCompleted()
|
||||
}
|
||||
|
||||
func (s *Torrent) BytesMissing() int64 {
|
||||
func (s *Controller) BytesMissing() int64 {
|
||||
<-s.t.GotInfo()
|
||||
return s.t.BytesMissing()
|
||||
}
|
||||
|
||||
func (s *Torrent) Length() int64 {
|
||||
func (s *Controller) Length() int64 {
|
||||
<-s.t.GotInfo()
|
||||
return s.t.Length()
|
||||
}
|
||||
|
||||
func (s *Torrent) Files(ctx context.Context) ([]*torrent.File, error) {
|
||||
func (s *Controller) Files(ctx context.Context) ([]*torrent.File, error) {
|
||||
fileMappings, err := s.rep.FileMappings(ctx, s.t.InfoHash())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -95,11 +94,11 @@ func Map[T, U any](ts []T, f func(T) U) []U {
|
|||
return us
|
||||
}
|
||||
|
||||
func (s *Torrent) ExcludeFile(ctx context.Context, f *torrent.File) error {
|
||||
func (s *Controller) ExcludeFile(ctx context.Context, f *torrent.File) error {
|
||||
return s.rep.ExcludeFile(ctx, f)
|
||||
}
|
||||
|
||||
func (s *Torrent) isFileComplete(startIndex int, endIndex int) bool {
|
||||
func (s *Controller) isFileComplete(startIndex int, endIndex int) bool {
|
||||
for i := startIndex; i < endIndex; i++ {
|
||||
if !s.t.Piece(i).State().Complete {
|
||||
return false
|
||||
|
@ -108,7 +107,7 @@ func (s *Torrent) isFileComplete(startIndex int, endIndex int) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func (s *Torrent) ValidateTorrent() error {
|
||||
func (s *Controller) ValidateTorrent() error {
|
||||
<-s.t.GotInfo()
|
||||
s.t.VerifyData()
|
||||
return nil
|
|
@ -1,8 +1,7 @@
|
|||
package store
|
||||
package torrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/anacrolix/torrent"
|
||||
|
@ -10,13 +9,13 @@ import (
|
|||
"github.com/royalcat/kv"
|
||||
)
|
||||
|
||||
func NewFileMappings(metaDir string, storage TorrentFileDeleter) (*FilesMappings, error) {
|
||||
func newFileMappingsStore(metaDir string, storage TorrentFileDeleter) (*filesMappingsStore, error) {
|
||||
str, err := kv.NewBadgerKVBytes[string, string](filepath.Join(metaDir, "file-mappings"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := &FilesMappings{
|
||||
r := &filesMappingsStore{
|
||||
mappings: str,
|
||||
storage: storage,
|
||||
}
|
||||
|
@ -24,13 +23,11 @@ func NewFileMappings(metaDir string, storage TorrentFileDeleter) (*FilesMappings
|
|||
return r, nil
|
||||
}
|
||||
|
||||
type FilesMappings struct {
|
||||
type filesMappingsStore struct {
|
||||
mappings kv.Store[string, string]
|
||||
storage TorrentFileDeleter
|
||||
}
|
||||
|
||||
var ErrNotFound = errors.New("not found")
|
||||
|
||||
type TorrentFileDeleter interface {
|
||||
DeleteFile(file *torrent.File) error
|
||||
}
|
||||
|
@ -39,15 +36,15 @@ func fileKey(file *torrent.File) string {
|
|||
return file.Torrent().InfoHash().HexString() + "/" + file.Path()
|
||||
}
|
||||
|
||||
func (r *FilesMappings) MapFile(ctx context.Context, file *torrent.File, target string) error {
|
||||
func (r *filesMappingsStore) MapFile(ctx context.Context, file *torrent.File, target string) error {
|
||||
return r.mappings.Set(ctx, fileKey(file), target)
|
||||
}
|
||||
|
||||
func (r *FilesMappings) ExcludeFile(ctx context.Context, file *torrent.File) error {
|
||||
func (r *filesMappingsStore) ExcludeFile(ctx context.Context, file *torrent.File) error {
|
||||
return r.mappings.Set(ctx, fileKey(file), "")
|
||||
}
|
||||
|
||||
func (r *FilesMappings) FileMappings(ctx context.Context, ih infohash.T) (map[string]string, error) {
|
||||
func (r *filesMappingsStore) FileMappings(ctx context.Context, ih infohash.T) (map[string]string, error) {
|
||||
out := map[string]string{}
|
||||
err := r.mappings.RangeWithPrefix(ctx, ih.HexString(), func(k, v string) bool {
|
||||
out[k] = v
|
||||
|
@ -56,6 +53,6 @@ func (r *FilesMappings) FileMappings(ctx context.Context, ih infohash.T) (map[st
|
|||
return out, err
|
||||
}
|
||||
|
||||
func (r *FilesMappings) Close(ctx context.Context) error {
|
||||
func (r *filesMappingsStore) Close(ctx context.Context) error {
|
||||
return r.mappings.Close(ctx)
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package vfs
|
||||
package torrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
@ -12,7 +12,7 @@ import (
|
|||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/controller"
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/vfs"
|
||||
"github.com/anacrolix/torrent"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
@ -23,23 +23,35 @@ type TorrentFS struct {
|
|||
name string
|
||||
|
||||
mu sync.Mutex
|
||||
Torrent *controller.Torrent
|
||||
Torrent *Controller
|
||||
|
||||
filesCache map[string]File
|
||||
filesCache map[string]vfs.File
|
||||
|
||||
lastAccessTimeout atomic.Pointer[time.Time]
|
||||
|
||||
resolver *resolver
|
||||
resolver *vfs.Resolver
|
||||
}
|
||||
|
||||
var _ Filesystem = (*TorrentFS)(nil)
|
||||
var _ vfs.Filesystem = (*TorrentFS)(nil)
|
||||
|
||||
func NewTorrentFs(name string, c *controller.Torrent) *TorrentFS {
|
||||
return &TorrentFS{
|
||||
name: name,
|
||||
Torrent: c,
|
||||
resolver: newResolver(ArchiveFactories),
|
||||
func (s *Service) NewTorrentFs(ctx context.Context, f vfs.File) (vfs.Filesystem, error) {
|
||||
defer f.Close(ctx)
|
||||
|
||||
info, err := f.Info()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c, err := s.LoadTorrent(ctx, f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &TorrentFS{
|
||||
name: info.Name(),
|
||||
Torrent: c,
|
||||
resolver: vfs.NewResolver(vfs.ArchiveFactories),
|
||||
}, nil
|
||||
}
|
||||
|
||||
var _ fs.DirEntry = (*TorrentFS)(nil)
|
||||
|
@ -89,7 +101,7 @@ func (tfs *TorrentFS) FsName() string {
|
|||
return "torrentfs"
|
||||
}
|
||||
|
||||
func (fs *TorrentFS) files(ctx context.Context) (map[string]File, error) {
|
||||
func (fs *TorrentFS) files(ctx context.Context) (map[string]vfs.File, error) {
|
||||
fs.mu.Lock()
|
||||
defer fs.mu.Unlock()
|
||||
|
||||
|
@ -105,10 +117,10 @@ func (fs *TorrentFS) files(ctx context.Context) (map[string]File, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
fs.filesCache = make(map[string]File)
|
||||
fs.filesCache = make(map[string]vfs.File)
|
||||
for _, file := range files {
|
||||
file.SetPriority(torrent.PiecePriorityNormal)
|
||||
p := AbsPath(file.Path())
|
||||
p := vfs.AbsPath(file.Path())
|
||||
tf, err := openTorrentFile(ctx, path.Base(p), file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -117,17 +129,17 @@ func (fs *TorrentFS) files(ctx context.Context) (map[string]File, error) {
|
|||
}
|
||||
|
||||
// TODO optional
|
||||
if len(fs.filesCache) == 1 && fs.resolver.isNestedFs(fs.Torrent.Name()) {
|
||||
if len(fs.filesCache) == 1 && fs.resolver.IsNestedFs(fs.Torrent.Name()) {
|
||||
filepath := "/" + fs.Torrent.Name()
|
||||
if file, ok := fs.filesCache[filepath]; ok {
|
||||
nestedFs, err := fs.resolver.nestedFs(ctx, filepath, file)
|
||||
nestedFs, err := fs.resolver.NestedFs(ctx, filepath, file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if nestedFs == nil {
|
||||
goto DEFAULT_DIR // FIXME
|
||||
}
|
||||
fs.filesCache, err = fs.listFilesRecursive(ctx, nestedFs, "/")
|
||||
fs.filesCache, err = listFilesRecursive(ctx, nestedFs, "/")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -149,7 +161,7 @@ DEFAULT_DIR:
|
|||
for k, f := range fs.filesCache {
|
||||
delete(fs.filesCache, k)
|
||||
k, _ = strings.CutPrefix(k, rootDir)
|
||||
k = AbsPath(k)
|
||||
k = vfs.AbsPath(k)
|
||||
fs.filesCache[k] = f
|
||||
}
|
||||
}
|
||||
|
@ -157,45 +169,22 @@ DEFAULT_DIR:
|
|||
return fs.filesCache, nil
|
||||
}
|
||||
|
||||
// func anyPeerHasFiles(file *torrent.File) bool {
|
||||
// for _, conn := range file.Torrent().PeerConns() {
|
||||
// if bitmapHaveFile(conn.PeerPieces(), file) {
|
||||
// return true
|
||||
// }
|
||||
// }
|
||||
// return false
|
||||
// }
|
||||
|
||||
// func bitmapHaveFile(bitmap *roaring.Bitmap, file *torrent.File) bool {
|
||||
// for i := file.BeginPieceIndex(); i < file.EndPieceIndex(); i++ {
|
||||
// if !bitmap.ContainsInt(i) {
|
||||
// return false
|
||||
// }
|
||||
// }
|
||||
// return true
|
||||
// }
|
||||
|
||||
func (fs *TorrentFS) listFilesRecursive(ctx context.Context, vfs Filesystem, start string) (map[string]File, error) {
|
||||
ctx, span := tracer.Start(ctx, "listFilesRecursive",
|
||||
fs.traceAttrs(attribute.String("start", start)),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
out := make(map[string]File, 0)
|
||||
entries, err := vfs.ReadDir(ctx, start)
|
||||
func listFilesRecursive(ctx context.Context, fs vfs.Filesystem, start string) (map[string]vfs.File, error) {
|
||||
out := make(map[string]vfs.File, 0)
|
||||
entries, err := fs.ReadDir(ctx, start)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
filename := path.Join(start, entry.Name())
|
||||
if entry.IsDir() {
|
||||
rec, err := fs.listFilesRecursive(ctx, vfs, filename)
|
||||
rec, err := listFilesRecursive(ctx, fs, filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
maps.Copy(out, rec)
|
||||
} else {
|
||||
file, err := vfs.Open(ctx, filename)
|
||||
file, err := fs.Open(ctx, filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -206,7 +195,7 @@ func (fs *TorrentFS) listFilesRecursive(ctx context.Context, vfs Filesystem, sta
|
|||
return out, nil
|
||||
}
|
||||
|
||||
func (fs *TorrentFS) rawOpen(ctx context.Context, filename string) (file File, err error) {
|
||||
func (fs *TorrentFS) rawOpen(ctx context.Context, filename string) (file vfs.File, err error) {
|
||||
ctx, span := tracer.Start(ctx, "rawOpen",
|
||||
fs.traceAttrs(attribute.String("filename", filename)),
|
||||
)
|
||||
|
@ -221,7 +210,7 @@ func (fs *TorrentFS) rawOpen(ctx context.Context, filename string) (file File, e
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file, err = getFile(files, filename)
|
||||
file, err = vfs.GetFile(files, filename)
|
||||
return file, err
|
||||
}
|
||||
|
||||
|
@ -236,7 +225,7 @@ func (fs *TorrentFS) rawStat(ctx context.Context, filename string) (fs.FileInfo,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
file, err := getFile(files, filename)
|
||||
file, err := vfs.GetFile(files, filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -258,11 +247,11 @@ func (tfs *TorrentFS) Stat(ctx context.Context, filename string) (fs.FileInfo, e
|
|||
)
|
||||
defer span.End()
|
||||
|
||||
if isRoot(filename) {
|
||||
if vfs.IsRoot(filename) {
|
||||
return tfs, nil
|
||||
}
|
||||
|
||||
fsPath, nestedFs, nestedFsPath, err := tfs.resolver.resolvePath(ctx, filename, tfs.rawOpen)
|
||||
fsPath, nestedFs, nestedFsPath, err := tfs.resolver.ResolvePath(ctx, filename, tfs.rawOpen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -287,17 +276,17 @@ func (tfs *TorrentFS) Stat(ctx context.Context, filename string) (fs.FileInfo, e
|
|||
return tfs.rawStat(ctx, fsPath)
|
||||
}
|
||||
|
||||
func (tfs *TorrentFS) Open(ctx context.Context, filename string) (file File, err error) {
|
||||
func (tfs *TorrentFS) Open(ctx context.Context, filename string) (file vfs.File, err error) {
|
||||
ctx, span := tracer.Start(ctx, "Open",
|
||||
tfs.traceAttrs(attribute.String("filename", filename)),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
if isRoot(filename) {
|
||||
return newDirFile(tfs.name), nil
|
||||
if vfs.IsRoot(filename) {
|
||||
return vfs.NewDirFile(tfs.name), nil
|
||||
}
|
||||
|
||||
fsPath, nestedFs, nestedFsPath, err := tfs.resolver.resolvePath(ctx, filename, tfs.rawOpen)
|
||||
fsPath, nestedFs, nestedFsPath, err := tfs.resolver.ResolvePath(ctx, filename, tfs.rawOpen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -328,7 +317,7 @@ func (tfs *TorrentFS) ReadDir(ctx context.Context, name string) ([]fs.DirEntry,
|
|||
)
|
||||
defer span.End()
|
||||
|
||||
fsPath, nestedFs, nestedFsPath, err := tfs.resolver.resolvePath(ctx, name, tfs.rawOpen)
|
||||
fsPath, nestedFs, nestedFsPath, err := tfs.resolver.ResolvePath(ctx, name, tfs.rawOpen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -354,7 +343,7 @@ func (tfs *TorrentFS) ReadDir(ctx context.Context, name string) ([]fs.DirEntry,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
return listDirFromFiles(files, fsPath)
|
||||
return vfs.ListDirFromFiles(files, fsPath)
|
||||
}
|
||||
|
||||
func (fs *TorrentFS) Unlink(ctx context.Context, name string) error {
|
||||
|
@ -363,7 +352,7 @@ func (fs *TorrentFS) Unlink(ctx context.Context, name string) error {
|
|||
)
|
||||
defer span.End()
|
||||
|
||||
name = AbsPath(name)
|
||||
name = vfs.AbsPath(name)
|
||||
|
||||
fs.mu.Lock()
|
||||
defer fs.mu.Unlock()
|
||||
|
@ -374,7 +363,7 @@ func (fs *TorrentFS) Unlink(ctx context.Context, name string) error {
|
|||
}
|
||||
|
||||
if !slices.Contains(maps.Keys(files), name) {
|
||||
return ErrNotExist
|
||||
return vfs.ErrNotExist
|
||||
}
|
||||
|
||||
file := files[name]
|
||||
|
@ -382,13 +371,13 @@ func (fs *TorrentFS) Unlink(ctx context.Context, name string) error {
|
|||
|
||||
tfile, ok := file.(*torrentFile)
|
||||
if !ok {
|
||||
return ErrNotImplemented
|
||||
return vfs.ErrNotImplemented
|
||||
}
|
||||
|
||||
return fs.Torrent.ExcludeFile(ctx, tfile.file)
|
||||
}
|
||||
|
||||
var _ File = (*torrentFile)(nil)
|
||||
var _ vfs.File = (*torrentFile)(nil)
|
||||
|
||||
type torrentFile struct {
|
||||
name string
|
||||
|
@ -437,11 +426,11 @@ func (tf *torrentFile) Name() string {
|
|||
|
||||
// Type implements File.
|
||||
func (tf *torrentFile) Type() fs.FileMode {
|
||||
return roMode | fs.ModeDir
|
||||
return vfs.ROMode | fs.ModeDir
|
||||
}
|
||||
|
||||
func (tf *torrentFile) Info() (fs.FileInfo, error) {
|
||||
return newFileInfo(tf.name, tf.file.Length()), nil
|
||||
return vfs.NewFileInfo(tf.name, tf.file.Length()), nil
|
||||
}
|
||||
|
||||
func (tf *torrentFile) Size() int64 {
|
|
@ -1,4 +1,4 @@
|
|||
package vfs
|
||||
package torrent
|
||||
|
||||
import (
|
||||
"os"
|
|
@ -1,7 +1,8 @@
|
|||
package store
|
||||
package torrent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
|
@ -12,11 +13,13 @@ import (
|
|||
"github.com/dgraph-io/badger/v4"
|
||||
)
|
||||
|
||||
type InfoBytes struct {
|
||||
var errNotFound = errors.New("not found")
|
||||
|
||||
type infoBytesStore struct {
|
||||
db *badger.DB
|
||||
}
|
||||
|
||||
func NewInfoBytes(metaDir string) (*InfoBytes, error) {
|
||||
func newInfoBytesStore(metaDir string) (*infoBytesStore, error) {
|
||||
l := slog.With("component", "badger", "db", "info-bytes")
|
||||
|
||||
opts := badger.
|
||||
|
@ -26,16 +29,16 @@ func NewInfoBytes(metaDir string) (*InfoBytes, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &InfoBytes{db}, nil
|
||||
return &infoBytesStore{db}, nil
|
||||
}
|
||||
|
||||
func (k *InfoBytes) GetBytes(ih infohash.T) ([]byte, error) {
|
||||
func (k *infoBytesStore) GetBytes(ih infohash.T) ([]byte, error) {
|
||||
var data []byte
|
||||
err := k.db.View(func(tx *badger.Txn) error {
|
||||
item, err := tx.Get(ih.Bytes())
|
||||
if err != nil {
|
||||
if err == badger.ErrKeyNotFound {
|
||||
return ErrNotFound
|
||||
return errNotFound
|
||||
}
|
||||
|
||||
return fmt.Errorf("error getting value: %w", err)
|
||||
|
@ -47,7 +50,7 @@ func (k *InfoBytes) GetBytes(ih infohash.T) ([]byte, error) {
|
|||
return data, err
|
||||
}
|
||||
|
||||
func (k *InfoBytes) Get(ih infohash.T) (*metainfo.MetaInfo, error) {
|
||||
func (k *infoBytesStore) Get(ih infohash.T) (*metainfo.MetaInfo, error) {
|
||||
data, err := k.GetBytes(ih)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -56,7 +59,7 @@ func (k *InfoBytes) Get(ih infohash.T) (*metainfo.MetaInfo, error) {
|
|||
return metainfo.Load(bytes.NewReader(data))
|
||||
}
|
||||
|
||||
func (me *InfoBytes) SetBytes(ih infohash.T, data []byte) error {
|
||||
func (me *infoBytesStore) SetBytes(ih infohash.T, data []byte) error {
|
||||
return me.db.Update(func(txn *badger.Txn) error {
|
||||
item, err := txn.Get(ih.Bytes())
|
||||
if err != nil {
|
||||
|
@ -75,16 +78,16 @@ func (me *InfoBytes) SetBytes(ih infohash.T, data []byte) error {
|
|||
})
|
||||
}
|
||||
|
||||
func (me *InfoBytes) Set(ih infohash.T, info metainfo.MetaInfo) error {
|
||||
func (me *infoBytesStore) Set(ih infohash.T, info metainfo.MetaInfo) error {
|
||||
return me.SetBytes(ih, info.InfoBytes)
|
||||
}
|
||||
|
||||
func (k *InfoBytes) Delete(ih infohash.T) error {
|
||||
func (k *infoBytesStore) Delete(ih infohash.T) error {
|
||||
return k.db.Update(func(txn *badger.Txn) error {
|
||||
return txn.Delete(ih.Bytes())
|
||||
})
|
||||
}
|
||||
|
||||
func (me *InfoBytes) Close() error {
|
||||
func (me *infoBytesStore) Close() error {
|
||||
return me.db.Close()
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package datastorage
|
||||
package torrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
@ -7,8 +7,7 @@ import (
|
|||
"os"
|
||||
"path"
|
||||
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/controller"
|
||||
"github.com/anacrolix/torrent"
|
||||
atorrent "github.com/anacrolix/torrent"
|
||||
"github.com/anacrolix/torrent/metainfo"
|
||||
"github.com/anacrolix/torrent/storage"
|
||||
"github.com/anacrolix/torrent/types/infohash"
|
||||
|
@ -81,17 +80,17 @@ func (p *PieceStorage) Close() error {
|
|||
}
|
||||
|
||||
// DeleteFile implements FileStorageDeleter.
|
||||
func (p *PieceStorage) DeleteFile(file *torrent.File) error {
|
||||
func (p *PieceStorage) DeleteFile(file *atorrent.File) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
// CleanupDirs implements DataStorage.
|
||||
func (p *PieceStorage) CleanupDirs(ctx context.Context, expected []*controller.Torrent, dryRun bool) (int, error) {
|
||||
func (p *PieceStorage) CleanupDirs(ctx context.Context, expected []*Controller, dryRun bool) (int, error) {
|
||||
return 0, nil // TODO
|
||||
}
|
||||
|
||||
// CleanupFiles implements DataStorage.
|
||||
func (p *PieceStorage) CleanupFiles(ctx context.Context, expected []*controller.Torrent, dryRun bool) (int, error) {
|
||||
func (p *PieceStorage) CleanupFiles(ctx context.Context, expected []*Controller, dryRun bool) (int, error) {
|
||||
return 0, nil // TODO
|
||||
}
|
||||
|
|
@ -1,22 +1,21 @@
|
|||
package service
|
||||
package torrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.kmsign.ru/royalcat/tstor/pkg/uuid"
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/controller"
|
||||
"github.com/anacrolix/torrent"
|
||||
"github.com/anacrolix/torrent/types/infohash"
|
||||
)
|
||||
|
||||
type TorrentDownloadTask struct {
|
||||
type DownloadTask struct {
|
||||
ID uuid.UUID
|
||||
InfoHash infohash.T
|
||||
File string
|
||||
}
|
||||
|
||||
func (s *Service) Download(ctx context.Context, task *TorrentDownloadTask) error {
|
||||
func (s *Service) Download(ctx context.Context, task *DownloadTask) error {
|
||||
t, ok := s.client.Torrent(task.InfoHash)
|
||||
if !ok {
|
||||
return fmt.Errorf("torrent with IH %s not found", task.InfoHash.HexString())
|
||||
|
@ -97,7 +96,7 @@ func (s *Service) Download(ctx context.Context, task *TorrentDownloadTask) error
|
|||
// }
|
||||
|
||||
type TorrentProgress struct {
|
||||
Torrent *controller.Torrent
|
||||
Torrent *Controller
|
||||
Current int64
|
||||
Total int64
|
||||
}
|
||||
|
@ -113,7 +112,7 @@ func (s *Service) DownloadProgress(ctx context.Context) (<-chan TorrentProgress,
|
|||
defer close(out)
|
||||
for _, t := range torrents {
|
||||
sub := t.Torrent().SubscribePieceStateChanges()
|
||||
go func(t *controller.Torrent) {
|
||||
go func(t *Controller) {
|
||||
for stateChange := range sub.Values {
|
||||
if !stateChange.Complete && !stateChange.Partial {
|
||||
continue
|
|
@ -1,4 +1,4 @@
|
|||
package service
|
||||
package torrent
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
|
@ -16,8 +16,6 @@ import (
|
|||
"git.kmsign.ru/royalcat/tstor/pkg/ctxio"
|
||||
"git.kmsign.ru/royalcat/tstor/pkg/rlog"
|
||||
"git.kmsign.ru/royalcat/tstor/src/config"
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/controller"
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/datastorage"
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/store"
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/tkv"
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/vfs"
|
||||
|
@ -35,7 +33,7 @@ import (
|
|||
"github.com/royalcat/kv"
|
||||
)
|
||||
|
||||
var tracer = otel.Tracer("git.kmsign.ru/royalcat/tstor/src/service")
|
||||
var tracer = otel.Tracer("git.kmsign.ru/royalcat/tstor/host/torrent")
|
||||
|
||||
type DirAquire struct {
|
||||
Name string
|
||||
|
@ -44,9 +42,9 @@ type DirAquire struct {
|
|||
|
||||
type Service struct {
|
||||
client *torrent.Client
|
||||
excludedFiles *store.FilesMappings
|
||||
infoBytes *store.InfoBytes
|
||||
Storage *datastorage.DataStorage
|
||||
excludedFiles *filesMappingsStore
|
||||
infoBytes *infoBytesStore
|
||||
Storage *DataStorage
|
||||
fis *store.FileItemStore
|
||||
dirsAquire kv.Store[string, DirAquire]
|
||||
|
||||
|
@ -58,7 +56,7 @@ type Service struct {
|
|||
log *rlog.Logger
|
||||
}
|
||||
|
||||
func New(sourceFs billy.Filesystem, conf config.TorrentClient) (*Service, error) {
|
||||
func NewService(sourceFs billy.Filesystem, conf config.TorrentClient) (*Service, error) {
|
||||
s := &Service{
|
||||
log: rlog.Component("torrent-service"),
|
||||
sourceFs: sourceFs,
|
||||
|
@ -76,17 +74,17 @@ func New(sourceFs billy.Filesystem, conf config.TorrentClient) (*Service, error)
|
|||
return nil, fmt.Errorf("error starting item store: %w", err)
|
||||
}
|
||||
|
||||
s.Storage, _, err = datastorage.Setup(conf)
|
||||
s.Storage, _, err = setupStorage(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.excludedFiles, err = store.NewFileMappings(conf.MetadataFolder, s.Storage)
|
||||
s.excludedFiles, err = newFileMappingsStore(conf.MetadataFolder, s.Storage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.infoBytes, err = store.NewInfoBytes(conf.MetadataFolder)
|
||||
s.infoBytes, err = newInfoBytesStore(conf.MetadataFolder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -132,7 +130,7 @@ func (s *Service) Close(ctx context.Context) error {
|
|||
)...)
|
||||
}
|
||||
|
||||
func (s *Service) LoadTorrent(ctx context.Context, f vfs.File) (*torrent.Torrent, error) {
|
||||
func (s *Service) LoadTorrent(ctx context.Context, f vfs.File) (*Controller, error) {
|
||||
ctx, span := tracer.Start(ctx, "LoadTorrent")
|
||||
defer span.End()
|
||||
log := s.log
|
||||
|
@ -171,7 +169,7 @@ func (s *Service) LoadTorrent(ctx context.Context, f vfs.File) (*torrent.Torrent
|
|||
if len(infoBytes) == 0 {
|
||||
log.Info(ctx, "no info loaded from file, try to load from cache")
|
||||
infoBytes, err = s.infoBytes.GetBytes(spec.InfoHash)
|
||||
if err != nil && err != store.ErrNotFound {
|
||||
if err != nil && err != errNotFound {
|
||||
return nil, fmt.Errorf("get info bytes from database: %w", err)
|
||||
}
|
||||
}
|
||||
|
@ -218,7 +216,7 @@ func (s *Service) LoadTorrent(ctx context.Context, f vfs.File) (*torrent.Torrent
|
|||
}
|
||||
}
|
||||
|
||||
return t, nil
|
||||
return newController(t, s.excludedFiles), nil
|
||||
}
|
||||
|
||||
func (s *Service) checkTorrentCompatable(ctx context.Context, ih infohash.T, info metainfo.Info) (compatable bool, tryLater bool, err error) {
|
||||
|
@ -335,38 +333,12 @@ func (s *Service) checkTorrentFilesCompatable(ctx context.Context, aq DirAquire,
|
|||
return true
|
||||
}
|
||||
|
||||
// func (s *Service) getTorrentsByName(name string) []*torrent.Torrent {
|
||||
// out := []*torrent.Torrent{}
|
||||
// for _, t := range s.c.Torrents() {
|
||||
// if t.Name() == name {
|
||||
// out = append(out, t)
|
||||
// }
|
||||
// }
|
||||
// return out
|
||||
// }
|
||||
|
||||
func isValidInfoHashBytes(d []byte) bool {
|
||||
var info metainfo.Info
|
||||
err := bencode.Unmarshal(d, &info)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (s *Service) NewTorrentFs(ctx context.Context, f vfs.File) (vfs.Filesystem, error) {
|
||||
defer f.Close(ctx)
|
||||
|
||||
info, err := f.Info()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t, err := s.LoadTorrent(ctx, f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return vfs.NewTorrentFs(info.Name(), controller.NewTorrent(t, s.excludedFiles)), nil
|
||||
}
|
||||
|
||||
func (s *Service) Stats() (*Stats, error) {
|
||||
return &Stats{}, nil
|
||||
}
|
||||
|
@ -435,17 +407,17 @@ func (s *Service) loadTorrentFiles(ctx context.Context) error {
|
|||
})
|
||||
}
|
||||
|
||||
func (s *Service) ListTorrents(ctx context.Context) ([]*controller.Torrent, error) {
|
||||
func (s *Service) ListTorrents(ctx context.Context) ([]*Controller, error) {
|
||||
<-s.torrentLoaded
|
||||
|
||||
out := []*controller.Torrent{}
|
||||
out := []*Controller{}
|
||||
for _, v := range s.client.Torrents() {
|
||||
out = append(out, controller.NewTorrent(v, s.excludedFiles))
|
||||
out = append(out, newController(v, s.excludedFiles))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetTorrent(infohashHex string) (*controller.Torrent, error) {
|
||||
func (s *Service) GetTorrent(infohashHex string) (*Controller, error) {
|
||||
<-s.torrentLoaded
|
||||
|
||||
t, ok := s.client.Torrent(infohash.FromHexString(infohashHex))
|
||||
|
@ -453,7 +425,7 @@ func (s *Service) GetTorrent(infohashHex string) (*controller.Torrent, error) {
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
return controller.NewTorrent(t, s.excludedFiles), nil
|
||||
return newController(t, s.excludedFiles), nil
|
||||
}
|
||||
|
||||
func slicesUnique[S ~[]E, E comparable](in S) S {
|
|
@ -1,4 +1,4 @@
|
|||
package datastorage
|
||||
package torrent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -10,7 +10,7 @@ import (
|
|||
"github.com/anacrolix/torrent/storage"
|
||||
)
|
||||
|
||||
func Setup(cfg config.TorrentClient) (*DataStorage, storage.PieceCompletion, error) {
|
||||
func setupStorage(cfg config.TorrentClient) (*DataStorage, storage.PieceCompletion, error) {
|
||||
pcp := filepath.Join(cfg.MetadataFolder, "piece-completion")
|
||||
if err := os.MkdirAll(pcp, 0744); err != nil {
|
||||
return nil, nil, fmt.Errorf("error creating piece completion folder: %w", err)
|
|
@ -1,4 +1,4 @@
|
|||
package service
|
||||
package torrent
|
||||
|
||||
import (
|
||||
"errors"
|
|
@ -1,4 +1,4 @@
|
|||
package datastorage
|
||||
package torrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
@ -12,27 +12,16 @@ import (
|
|||
"path/filepath"
|
||||
"slices"
|
||||
|
||||
"git.kmsign.ru/royalcat/tstor/src/host/controller"
|
||||
"github.com/anacrolix/torrent"
|
||||
"github.com/anacrolix/torrent/metainfo"
|
||||
"github.com/anacrolix/torrent/storage"
|
||||
"github.com/dustin/go-humanize"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// type DataStorage interface {
|
||||
// storage.ClientImplCloser
|
||||
// DeleteFile(file *torrent.File) error
|
||||
// CleanupDirs(ctx context.Context, expected []*controller.Torrent, dryRun bool) (int, error)
|
||||
// CleanupFiles(ctx context.Context, expected []*controller.Torrent, dryRun bool) (int, error)
|
||||
// }
|
||||
|
||||
var tracer = otel.Tracer("git.kmsign.ru/royalcat/tstor/src/host/datastorage")
|
||||
|
||||
// NewFileStorage creates a new ClientImplCloser that stores files using the OS native filesystem.
|
||||
func NewFileStorage(baseDir string, pc storage.PieceCompletion) *DataStorage {
|
||||
return &DataStorage{
|
||||
|
@ -100,7 +89,7 @@ func (fs *DataStorage) DeleteFile(file *torrent.File) error {
|
|||
return os.Remove(filePath)
|
||||
}
|
||||
|
||||
func (fs *DataStorage) CleanupDirs(ctx context.Context, expected []*controller.Torrent, dryRun bool) ([]string, error) {
|
||||
func (fs *DataStorage) CleanupDirs(ctx context.Context, expected []*Controller, dryRun bool) ([]string, error) {
|
||||
log := fs.log.With("function", "CleanupDirs", "expectedTorrents", len(expected), "dryRun", dryRun)
|
||||
|
||||
expectedEntries := []string{}
|
||||
|
@ -139,7 +128,7 @@ func (fs *DataStorage) CleanupDirs(ctx context.Context, expected []*controller.T
|
|||
return toDelete, nil
|
||||
}
|
||||
|
||||
func (s *DataStorage) CleanupFiles(ctx context.Context, expected []*controller.Torrent, dryRun bool) ([]string, error) {
|
||||
func (s *DataStorage) CleanupFiles(ctx context.Context, expected []*Controller, dryRun bool) ([]string, error) {
|
||||
log := s.log.With("function", "CleanupFiles", "expectedTorrents", len(expected), "dryRun", dryRun)
|
||||
|
||||
expectedEntries := []string{}
|
|
@ -122,11 +122,11 @@ func (a *ArchiveFS) Unlink(ctx context.Context, filename string) error {
|
|||
}
|
||||
|
||||
func (a *ArchiveFS) Open(ctx context.Context, filename string) (File, error) {
|
||||
return getFile(a.files, filename)
|
||||
return GetFile(a.files, filename)
|
||||
}
|
||||
|
||||
func (a *ArchiveFS) ReadDir(ctx context.Context, path string) ([]fs.DirEntry, error) {
|
||||
return listDirFromFiles(a.files, path)
|
||||
return ListDirFromFiles(a.files, path)
|
||||
}
|
||||
|
||||
// Stat implements Filesystem.
|
||||
|
@ -198,11 +198,11 @@ func (d *archiveFile) Name() string {
|
|||
|
||||
// Type implements File.
|
||||
func (d *archiveFile) Type() fs.FileMode {
|
||||
return roMode
|
||||
return ROMode
|
||||
}
|
||||
|
||||
func (d *archiveFile) Info() (fs.FileInfo, error) {
|
||||
return newFileInfo(d.name, d.size), nil
|
||||
return NewFileInfo(d.name, d.size), nil
|
||||
}
|
||||
|
||||
func (d *archiveFile) Size() int64 {
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
|
||||
var _ File = &dirFile{}
|
||||
|
||||
func newDirFile(name string) File {
|
||||
func NewDirFile(name string) File {
|
||||
return &dirFile{
|
||||
name: path.Base(name),
|
||||
}
|
||||
|
@ -55,5 +55,5 @@ func (d *dirFile) Size() int64 {
|
|||
|
||||
// Type implements File.
|
||||
func (d *dirFile) Type() fs.FileMode {
|
||||
return roMode | fs.ModeDir
|
||||
return ROMode | fs.ModeDir
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ func (d *DummyFs) FsName() string {
|
|||
|
||||
// Stat implements Filesystem.
|
||||
func (*DummyFs) Stat(ctx context.Context, filename string) (fs.FileInfo, error) {
|
||||
return newFileInfo(path.Base(filename), 0), nil // TODO
|
||||
return NewFileInfo(path.Base(filename), 0), nil // TODO
|
||||
}
|
||||
|
||||
func (d *DummyFs) Open(ctx context.Context, filename string) (File, error) {
|
||||
|
@ -55,8 +55,8 @@ func (d *DummyFs) Unlink(ctx context.Context, filename string) error {
|
|||
func (d *DummyFs) ReadDir(ctx context.Context, path string) ([]fs.DirEntry, error) {
|
||||
if path == "/dir/here" {
|
||||
return []fs.DirEntry{
|
||||
newFileInfo("file1.txt", 0),
|
||||
newFileInfo("file2.txt", 0),
|
||||
NewFileInfo("file1.txt", 0),
|
||||
NewFileInfo("file2.txt", 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -101,7 +101,7 @@ func (d *DummyFile) Type() fs.FileMode {
|
|||
|
||||
// Stat implements File.
|
||||
func (d *DummyFile) Info() (fs.FileInfo, error) {
|
||||
return newFileInfo(d.name, 0), nil
|
||||
return NewFileInfo(d.name, 0), nil
|
||||
}
|
||||
|
||||
func (d *DummyFile) Size() int64 {
|
||||
|
|
|
@ -44,7 +44,7 @@ type Filesystem interface {
|
|||
}
|
||||
|
||||
// readonly
|
||||
const roMode = fs.FileMode(0555)
|
||||
const ROMode = fs.FileMode(0555)
|
||||
|
||||
type fileInfo struct {
|
||||
name string
|
||||
|
@ -63,7 +63,7 @@ func newDirInfo(name string) *fileInfo {
|
|||
}
|
||||
}
|
||||
|
||||
func newFileInfo(name string, size int64) *fileInfo {
|
||||
func NewFileInfo(name string, size int64) *fileInfo {
|
||||
return &fileInfo{
|
||||
name: path.Base(name),
|
||||
size: size,
|
||||
|
@ -93,10 +93,10 @@ func (fi *fileInfo) Size() int64 {
|
|||
|
||||
func (fi *fileInfo) Mode() fs.FileMode {
|
||||
if fi.isDir {
|
||||
return roMode | fs.ModeDir
|
||||
return ROMode | fs.ModeDir
|
||||
}
|
||||
|
||||
return roMode
|
||||
return ROMode
|
||||
}
|
||||
|
||||
func (fi *fileInfo) ModTime() time.Time {
|
||||
|
|
|
@ -12,7 +12,7 @@ func TestFileinfo(t *testing.T) {
|
|||
|
||||
require := require.New(t)
|
||||
|
||||
fi := newFileInfo("abc/name", 42)
|
||||
fi := NewFileInfo("abc/name", 42)
|
||||
|
||||
require.Equal("name", fi.Name())
|
||||
require.False(fi.IsDir())
|
||||
|
@ -37,7 +37,7 @@ func TestDirInfo(t *testing.T) {
|
|||
require.NotNil(fi.ModTime())
|
||||
require.NotZero(fi.Type() & fs.ModeDir)
|
||||
require.NotZero(fi.Mode() & fs.ModeDir)
|
||||
require.Equal(roMode|fs.ModeDir, fi.Mode())
|
||||
require.Equal(ROMode|fs.ModeDir, fi.Mode())
|
||||
require.Nil(fi.Sys())
|
||||
|
||||
}
|
||||
|
|
|
@ -68,11 +68,11 @@ func NewMemoryFS(name string, files map[string]*MemoryFile) *MemoryFs {
|
|||
}
|
||||
|
||||
func (m *MemoryFs) Open(ctx context.Context, filename string) (File, error) {
|
||||
return getFile(m.files, filename)
|
||||
return GetFile(m.files, filename)
|
||||
}
|
||||
|
||||
func (fs *MemoryFs) ReadDir(ctx context.Context, path string) ([]fs.DirEntry, error) {
|
||||
return listDirFromFiles(fs.files, path)
|
||||
return ListDirFromFiles(fs.files, path)
|
||||
}
|
||||
|
||||
// Stat implements Filesystem.
|
||||
|
@ -81,7 +81,7 @@ func (mfs *MemoryFs) Stat(ctx context.Context, filename string) (fs.FileInfo, er
|
|||
if !ok {
|
||||
return nil, ErrNotExist
|
||||
}
|
||||
return newFileInfo(path.Base(filename), file.Size()), nil
|
||||
return NewFileInfo(path.Base(filename), file.Size()), nil
|
||||
}
|
||||
|
||||
// Unlink implements Filesystem.
|
||||
|
@ -110,11 +110,11 @@ func (d *MemoryFile) Name() string {
|
|||
|
||||
// Type implements File.
|
||||
func (d *MemoryFile) Type() fs.FileMode {
|
||||
return roMode
|
||||
return ROMode
|
||||
}
|
||||
|
||||
func (d *MemoryFile) Info() (fs.FileInfo, error) {
|
||||
return newFileInfo(d.name, int64(d.data.Len())), nil
|
||||
return NewFileInfo(d.name, int64(d.data.Len())), nil
|
||||
}
|
||||
|
||||
func (d *MemoryFile) Size() int64 {
|
||||
|
|
|
@ -34,8 +34,8 @@ func (fs *OsFS) Unlink(ctx context.Context, filename string) error {
|
|||
|
||||
// Open implements Filesystem.
|
||||
func (fs *OsFS) Open(ctx context.Context, filename string) (File, error) {
|
||||
if isRoot(filename) {
|
||||
return newDirFile(fs.Name()), nil
|
||||
if IsRoot(filename) {
|
||||
return NewDirFile(fs.Name()), nil
|
||||
}
|
||||
|
||||
return NewLazyOsFile(path.Join(fs.hostDir, filename))
|
||||
|
|
|
@ -21,7 +21,7 @@ import (
|
|||
|
||||
type ResolverFS struct {
|
||||
rootFS Filesystem
|
||||
resolver *resolver
|
||||
resolver *Resolver
|
||||
|
||||
log *rlog.Logger
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ type ResolverFS struct {
|
|||
func NewResolveFS(rootFs Filesystem, factories map[string]FsFactory) *ResolverFS {
|
||||
return &ResolverFS{
|
||||
rootFS: rootFs,
|
||||
resolver: newResolver(factories),
|
||||
resolver: NewResolver(factories),
|
||||
log: rlog.Component("fs.resolverfs"),
|
||||
}
|
||||
}
|
||||
|
@ -77,10 +77,10 @@ func (r *ResolverFS) Open(ctx context.Context, filename string) (File, error) {
|
|||
defer span.End()
|
||||
|
||||
if path.Clean(filename) == Separator {
|
||||
return newDirFile(r.Name()), nil
|
||||
return NewDirFile(r.Name()), nil
|
||||
}
|
||||
|
||||
fsPath, nestedFs, nestedFsPath, err := r.resolver.resolvePath(ctx, filename, r.rootFS.Open)
|
||||
fsPath, nestedFs, nestedFsPath, err := r.resolver.ResolvePath(ctx, filename, r.rootFS.Open)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -98,7 +98,7 @@ func (r *ResolverFS) ReadDir(ctx context.Context, dir string) ([]fs.DirEntry, er
|
|||
)
|
||||
defer span.End()
|
||||
|
||||
fsPath, nestedFs, nestedFsPath, err := r.resolver.resolvePath(ctx, dir, r.rootFS.Open)
|
||||
fsPath, nestedFs, nestedFsPath, err := r.resolver.ResolvePath(ctx, dir, r.rootFS.Open)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -112,7 +112,7 @@ func (r *ResolverFS) ReadDir(ctx context.Context, dir string) ([]fs.DirEntry, er
|
|||
}
|
||||
out := make([]fs.DirEntry, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
if r.resolver.isNestedFs(e.Name()) {
|
||||
if r.resolver.IsNestedFs(e.Name()) {
|
||||
filepath := path.Join("/", dir, e.Name())
|
||||
file, err := r.Open(ctx, filepath)
|
||||
if err != nil {
|
||||
|
@ -123,7 +123,7 @@ func (r *ResolverFS) ReadDir(ctx context.Context, dir string) ([]fs.DirEntry, er
|
|||
err = func() error {
|
||||
factoryCtx, cancel := subTimeout(ctx)
|
||||
defer cancel()
|
||||
nestedfs, err := r.resolver.nestedFs(factoryCtx, filepath, file)
|
||||
nestedfs, err := r.resolver.NestedFs(factoryCtx, filepath, file)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
r.log.Error(ctx, "creating fs timed out",
|
||||
|
@ -155,11 +155,11 @@ func (r *ResolverFS) Stat(ctx context.Context, filename string) (fs.FileInfo, er
|
|||
)
|
||||
defer span.End()
|
||||
|
||||
if isRoot(filename) {
|
||||
if IsRoot(filename) {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
fsPath, nestedFs, nestedFsPath, err := r.resolver.resolvePath(ctx, filename, r.rootFS.Open)
|
||||
fsPath, nestedFs, nestedFsPath, err := r.resolver.ResolvePath(ctx, filename, r.rootFS.Open)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -175,7 +175,7 @@ func (r *ResolverFS) Stat(ctx context.Context, filename string) (fs.FileInfo, er
|
|||
|
||||
// Unlink implements Filesystem.
|
||||
func (r *ResolverFS) Unlink(ctx context.Context, filename string) error {
|
||||
fsPath, nestedFs, nestedFsPath, err := r.resolver.resolvePath(ctx, filename, r.rootFS.Open)
|
||||
fsPath, nestedFs, nestedFsPath, err := r.resolver.ResolvePath(ctx, filename, r.rootFS.Open)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -210,14 +210,14 @@ var _ Filesystem = &ResolverFS{}
|
|||
|
||||
type FsFactory func(ctx context.Context, f File) (Filesystem, error)
|
||||
|
||||
func newResolver(factories map[string]FsFactory) *resolver {
|
||||
return &resolver{
|
||||
func NewResolver(factories map[string]FsFactory) *Resolver {
|
||||
return &Resolver{
|
||||
factories: factories,
|
||||
fsmap: map[string]Filesystem{},
|
||||
}
|
||||
}
|
||||
|
||||
type resolver struct {
|
||||
type Resolver struct {
|
||||
m sync.Mutex
|
||||
factories map[string]FsFactory
|
||||
fsmap map[string]Filesystem // filesystem cache
|
||||
|
@ -226,7 +226,7 @@ type resolver struct {
|
|||
|
||||
type openFile func(ctx context.Context, path string) (File, error)
|
||||
|
||||
func (r *resolver) isNestedFs(f string) bool {
|
||||
func (r *Resolver) IsNestedFs(f string) bool {
|
||||
for ext := range r.factories {
|
||||
if strings.HasSuffix(f, ext) {
|
||||
return true
|
||||
|
@ -235,7 +235,7 @@ func (r *resolver) isNestedFs(f string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (r *resolver) nestedFs(ctx context.Context, fsPath string, file File) (Filesystem, error) {
|
||||
func (r *Resolver) NestedFs(ctx context.Context, fsPath string, file File) (Filesystem, error) {
|
||||
for ext, nestFactory := range r.factories {
|
||||
if !strings.HasSuffix(fsPath, ext) {
|
||||
continue
|
||||
|
@ -258,7 +258,7 @@ func (r *resolver) nestedFs(ctx context.Context, fsPath string, file File) (File
|
|||
}
|
||||
|
||||
// open requeue raw open, without resolver call
|
||||
func (r *resolver) resolvePath(ctx context.Context, name string, rawOpen openFile) (fsPath string, nestedFs Filesystem, nestedFsPath string, err error) {
|
||||
func (r *Resolver) ResolvePath(ctx context.Context, name string, rawOpen openFile) (fsPath string, nestedFs Filesystem, nestedFsPath string, err error) {
|
||||
ctx, span := tracer.Start(ctx, "resolvePath")
|
||||
defer span.End()
|
||||
|
||||
|
@ -321,9 +321,9 @@ PARTS_LOOP:
|
|||
|
||||
var ErrNotExist = fs.ErrNotExist
|
||||
|
||||
func getFile[F File](m map[string]F, name string) (File, error) {
|
||||
func GetFile[F File](m map[string]F, name string) (File, error) {
|
||||
if name == Separator {
|
||||
return newDirFile(name), nil
|
||||
return NewDirFile(name), nil
|
||||
}
|
||||
|
||||
f, ok := m[name]
|
||||
|
@ -333,21 +333,21 @@ func getFile[F File](m map[string]F, name string) (File, error) {
|
|||
|
||||
for p := range m {
|
||||
if strings.HasPrefix(p, name) {
|
||||
return newDirFile(name), nil
|
||||
return NewDirFile(name), nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, ErrNotExist
|
||||
}
|
||||
|
||||
func listDirFromFiles[F File](m map[string]F, name string) ([]fs.DirEntry, error) {
|
||||
func ListDirFromFiles[F File](m map[string]F, name string) ([]fs.DirEntry, error) {
|
||||
out := make([]fs.DirEntry, 0, len(m))
|
||||
name = AddTrailSlash(path.Clean(name))
|
||||
for p, f := range m {
|
||||
if strings.HasPrefix(p, name) {
|
||||
parts := strings.Split(trimRelPath(p, name), Separator)
|
||||
if len(parts) == 1 {
|
||||
out = append(out, newFileInfo(parts[0], f.Size()))
|
||||
out = append(out, NewFileInfo(parts[0], f.Size()))
|
||||
} else {
|
||||
out = append(out, newDirInfo(parts[0]))
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
|
||||
const Separator = "/"
|
||||
|
||||
func isRoot(filename string) bool {
|
||||
func IsRoot(filename string) bool {
|
||||
return path.Clean(filename) == Separator
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue