From ec83e3b08bd713870853e2ab10250601ac3b1d75 Mon Sep 17 00:00:00 2001
From: royalcat <k.adamovich20@gmail.com>
Date: Wed, 18 Oct 2023 12:52:48 +0300
Subject: [PATCH 01/18] docker deploy

---
 .github/workflows/codeql-analysis.yml |  59 -------
 .github/workflows/docker.yaml         |  18 +-
 Dockerfile                            |  41 ++---
 build_tools/Dockerfile                |  19 ---
 cmd/tstor/main.go                     |   5 +-
 http_fs.go => embed.go                |   0
 src/host/torrent/service.go           |   4 +
 src/host/vfs/resolver.go              |  61 ++++---
 src/host/vfs/resolver_test.go         | 232 ++++++++++++--------------
 src/host/vfs/torrent.go               |  34 ++--
 src/host/vfs/torrent_test.go          |   2 +
 src/host/vfs/utils.go                 |  43 +----
 src/mounts/fuse/handler.go            |   4 +-
 src/mounts/fuse/handler_nocgo.go      |  23 +++
 src/mounts/fuse/mount.go              |  24 +--
 src/mounts/fuse/mount_test.go         |   2 +
 16 files changed, 236 insertions(+), 335 deletions(-)
 delete mode 100644 .github/workflows/codeql-analysis.yml
 delete mode 100644 build_tools/Dockerfile
 rename http_fs.go => embed.go (100%)
 create mode 100644 src/mounts/fuse/handler_nocgo.go

diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
deleted file mode 100644
index 5fa1be4..0000000
--- a/.github/workflows/codeql-analysis.yml
+++ /dev/null
@@ -1,59 +0,0 @@
-name: "CodeQL"
-
-on:
-  push:
-    branches: [ master ]
-  pull_request:
-    # The branches below must be a subset of the branches above
-    branches: [ master ]
-  schedule:
-    - cron: '26 14 * * 1'
-
-jobs:
-  analyze:
-    name: Analyze
-    runs-on: ubuntu-latest
-    permissions:
-      actions: read
-      contents: read
-      security-events: write
-
-    strategy:
-      fail-fast: false
-      matrix:
-        language: [ 'go', 'javascript' ]
-        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
-        # Learn more about CodeQL language support at https://git.io/codeql-language-support
-
-    steps:
-    - name: Checkout repository
-      uses: actions/checkout@v4
-
-    # Initializes the CodeQL tools for scanning.
-    - name: Initialize CodeQL
-      uses: github/codeql-action/init@v2
-      with:
-        languages: ${{ matrix.language }}
-        # If you wish to specify custom queries, you can do so here or in a config file.
-        # By default, queries listed here will override any specified in a config file.
-        # Prefix the list here with "+" to use these queries and those in the config file.
-        # queries: ./path/to/local/query, your-org/your-repo/queries@main
-
-    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
-    # If this step fails, then you should remove it and run the build manually (see below)
-    - name: Autobuild
-      uses: github/codeql-action/autobuild@v2
-
-    # â„šī¸ Command-line programs to run using the OS shell.
-    # 📚 https://git.io/JvXDl
-
-    # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines
-    #    and modify them (or add more) to build your code if your project
-    #    uses a compiled language
-
-    #- run: |
-    #   make bootstrap
-    #   make release
-
-    - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@v2
diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml
index 3c7e138..f83b1cd 100644
--- a/.github/workflows/docker.yaml
+++ b/.github/workflows/docker.yaml
@@ -9,6 +9,9 @@ on:
 
 jobs:
   build-docker:
+    permissions:
+      contents: read
+      packages: write
     strategy:
       fail-fast: false
       matrix:
@@ -16,22 +19,23 @@ jobs:
           - linux/amd64
           - linux/arm64
           - linux/386
-          - inux/arm/v6
+          - linux/arm/v5
           - linux/arm/v7
-          - linux/arm64
+          - linux/arm64/v8
+          - linux/riscv64
     runs-on: ubuntu-latest
     steps:
       - name: Checkout repository
         uses: actions/checkout@v3
 
       - name: Set up QEMU
-        uses: docker/setup-qemu-action@v2
+        uses: docker/setup-qemu-action@v3
 
       - name: Set up Docker Buildx
-        uses: docker/setup-buildx-action@v2
+        uses: docker/setup-buildx-action@v3
 
-      - name: Login to GitHub Container Registry
-        uses: docker/login-action@v2
+      - name: Login to Container Registry
+        uses: docker/login-action@v3
         with:
           registry: ghcr.io
           username: ${{ github.actor }}
@@ -54,7 +58,7 @@ jobs:
             type=sha
 
       - name: Build and push Docker image
-        uses: docker/build-push-action@v4
+        uses: docker/build-push-action@v5
         with:
           context: .
           push: true
diff --git a/Dockerfile b/Dockerfile
index d75f316..ebc4e1b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,33 +1,24 @@
-#===============
-# Stage 1: Build
-#===============
+FROM golang:1.21 as builder
 
-FROM golang:1.20 as builder
+WORKDIR /app
 
-ENV BIN_REPO=git.kmsign.ru/royalcat/tstor
-ENV BIN_PATH=$GOPATH/src/$BIN_REPO
+COPY go.mod ./
+COPY go.sum ./
+RUN go mod download
 
-COPY . $BIN_PATH
-WORKDIR $BIN_PATH
+COPY ./src ./src
+COPY ./cmd ./cmd
+COPY ./assets ./assets
+COPY ./templates ./templates
+COPY embed.go embed.go
 
-RUN apk add fuse-dev git gcc libc-dev g++ make
+RUN go generate ./...
+RUN CGO_ENABLED=0 go build -tags timetzdata -o /tstor ./cmd/tstor/main.go 
 
-RUN BIN_OUTPUT=/bin/tstor make build
 
-#===============
-# Stage 2: Run
-#===============
+FROM scratch
 
-FROM alpine:3
+COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
+COPY --from=builder /tstor /tstor
 
-RUN apk add gcc libc-dev fuse-dev
-
-COPY --from=builder /bin/tstor /bin/tstor
-RUN chmod +x /bin/tstor
-
-RUN mkdir /tstor-data
-
-RUN echo "user_allow_other" >> /etc/fuse.conf
-ENV tstor_FUSE_ALLOW_OTHER=true
-
-ENTRYPOINT ["./bin/tstor"]
+ENTRYPOINT ["/tstor"]
diff --git a/build_tools/Dockerfile b/build_tools/Dockerfile
deleted file mode 100644
index d21a348..0000000
--- a/build_tools/Dockerfile
+++ /dev/null
@@ -1,19 +0,0 @@
-FROM techknowlogick/xgo:go-1.17.x
-
-# add 32-bit and 64-bit architectures and install 7zip
-RUN \
-    dpkg --add-architecture i386 && \
-    dpkg --add-architecture amd64 && \
-    apt-get update && \
-    apt-get install -y --no-install-recommends p7zip-full
-
-# install LIBFUSE
-RUN \
-    apt-get update && \
-    apt-get install -y --no-install-recommends libfuse-dev:i386 && \
-    apt-get install -y --no-install-recommends libfuse-dev:amd64 && \
-    apt-get download libfuse-dev:i386 && \
-    dpkg -x libfuse-dev*i386*.deb /
-
-ENV \
-    OSXCROSS_NO_INCLUDE_PATH_WARNINGS 1
diff --git a/cmd/tstor/main.go b/cmd/tstor/main.go
index 979118d..ca84dab 100644
--- a/cmd/tstor/main.go
+++ b/cmd/tstor/main.go
@@ -27,7 +27,6 @@ import (
 
 const (
 	configFlag     = "config"
-	fuseAllowOther = "fuse-allow-other"
 	portFlag       = "http-port"
 	webDAVPortFlag = "webdav-port"
 )
@@ -45,7 +44,7 @@ func main() {
 		},
 
 		Action: func(c *cli.Context) error {
-			err := load(c.String(configFlag), c.Int(portFlag), c.Int(webDAVPortFlag), c.Bool(fuseAllowOther))
+			err := load(c.String(configFlag))
 
 			// stop program execution on errors to avoid flashing consoles
 			if err != nil && runtime.GOOS == "windows" {
@@ -108,7 +107,7 @@ func (s *stc) Close() error {
 	return nil
 }
 
-func load(configPath string, port, webDAVPort int, fuseAllowOther bool) error {
+func load(configPath string) error {
 	conf, err := config.Load(configPath)
 	if err != nil {
 		return fmt.Errorf("error loading configuration: %w", err)
diff --git a/http_fs.go b/embed.go
similarity index 100%
rename from http_fs.go
rename to embed.go
diff --git a/src/host/torrent/service.go b/src/host/torrent/service.go
index de87544..b251aff 100644
--- a/src/host/torrent/service.go
+++ b/src/host/torrent/service.go
@@ -47,6 +47,10 @@ func (s *Service) NewTorrentFs(f vfs.File) (vfs.Filesystem, error) {
 	}
 	<-t.GotInfo()
 	t.AllowDataDownload()
+	for _, f := range t.Files() {
+		f.SetPriority(torrent.PiecePriorityReadahead)
+	}
+
 	return vfs.NewTorrentFs(t, s.readTimeout), nil
 }
 
diff --git a/src/host/vfs/resolver.go b/src/host/vfs/resolver.go
index 2b058e8..df1ffd8 100644
--- a/src/host/vfs/resolver.go
+++ b/src/host/vfs/resolver.go
@@ -2,6 +2,7 @@ package vfs
 
 import (
 	"fmt"
+	"io/fs"
 	"strings"
 	"sync"
 )
@@ -88,11 +89,11 @@ PARTS_LOOP:
 	}
 
 	if nestOn == -1 {
-		return name, nil, "", nil
+		return clean(name), nil, "", nil
 	}
 
-	fsPath = Clean(strings.Join(parts[:nestOn], Separator))
-	nestedFsPath = Clean(strings.Join(parts[nestOn:], Separator))
+	fsPath = clean(strings.Join(parts[:nestOn], Separator))
+	nestedFsPath = clean(strings.Join(parts[nestOn:], Separator))
 
 	// we dont need lock until now
 	// it must be before fsmap read to exclude race condition:
@@ -119,28 +120,42 @@ PARTS_LOOP:
 
 }
 
-// func (r *resolver) resolveFile(name string, fs Filesystem) (File, error) {
-// 	fsPath, nestedFs, nestedFsPath, err := r.resolvePath(name, fs)
-// 	if err != nil {
-// 		return nil, err
-// 	}
+var ErrNotExist = fs.ErrNotExist
 
-// 	if nestedFs == nil {
-// 		return fs.Open(fsPath)
-// 	}
+func getFile[F File](m map[string]F, name string) (File, error) {
+	name = clean(name)
+	if name == Separator {
+		return &Dir{}, nil
+	}
 
-// 	return nestedFs.Open(nestedFsPath)
-// }
+	f, ok := m[name]
+	if ok {
+		return f, nil
+	}
 
-// func (r *resolver) resolveDir(name string, fs Filesystem) (map[string]File, error) {
-// 	fsPath, nestedFs, nestedFsPath, err := r.resolvePath(name, fs)
-// 	if err != nil {
-// 		return nil, err
-// 	}
+	for p := range m {
+		if strings.HasPrefix(p, name) {
+			return &Dir{}, nil
+		}
+	}
 
-// 	if nestedFs == nil {
-// 		return fs.ReadDir(fsPath)
-// 	}
+	return nil, ErrNotExist
+}
 
-// 	return nestedFs.ReadDir(nestedFsPath)
-// }
+func listFilesInDir[F File](m map[string]F, name string) (map[string]File, error) {
+	name = clean(name)
+
+	out := map[string]File{}
+	for p, f := range m {
+		if strings.HasPrefix(p, name) {
+			parts := strings.Split(trimRelPath(p, name), Separator)
+			if len(parts) == 1 {
+				out[parts[0]] = f
+			} else {
+				out[parts[0]] = &Dir{}
+			}
+		}
+	}
+
+	return out, nil
+}
diff --git a/src/host/vfs/resolver_test.go b/src/host/vfs/resolver_test.go
index 953ad82..401459f 100644
--- a/src/host/vfs/resolver_test.go
+++ b/src/host/vfs/resolver_test.go
@@ -2,6 +2,9 @@ package vfs
 
 import (
 	"os"
+	"testing"
+
+	"github.com/stretchr/testify/require"
 )
 
 type Dummy struct {
@@ -49,144 +52,121 @@ func (d *DummyFs) ReadDir(path string) (map[string]File, error) {
 
 var _ Filesystem = &DummyFs{}
 
-// func TestDefaultFactories(t *testing.T) {
-// 	t.Parallel()
+func TestResolver(t *testing.T) {
+	t.Parallel()
+	resolver := newResolver(ArchiveFactories)
+	t.Run("nested fs", func(t *testing.T) {
+		t.Parallel()
+		require := require.New(t)
 
-// 	require := require.New(t)
+		fsPath, nestedFs, nestedFsPath, err := resolver.resolvePath("/f1.rar/f2.rar", func(path string) (File, error) {
+			require.Equal("/f1.rar", path)
+			return &Dummy{}, nil
+		})
+		require.Nil(err)
+		require.Equal("/f1.rar", fsPath)
+		require.Equal("/f2.rar", nestedFsPath)
+		require.IsType(&archive{}, nestedFs)
+	})
+	t.Run("root", func(t *testing.T) {
+		t.Parallel()
+		require := require.New(t)
 
-// 	require.Contains(defaultFactories, ".zip")
-// 	require.Contains(defaultFactories, ".rar")
-// 	require.Contains(defaultFactories, ".7z")
+		fsPath, nestedFs, nestedFsPath, err := resolver.resolvePath("/", func(path string) (File, error) {
+			require.Equal("/", path)
+			return &Dummy{}, nil
+		})
+		require.Nil(err)
+		require.Nil(nestedFs)
+		require.Equal("/", fsPath)
+		require.Equal("", nestedFsPath)
+	})
 
-// 	fs, err := defaultFactories[".zip"](&Dummy{}, nil)
-// 	require.NoError(err)
-// 	require.NotNil(fs)
+	t.Run("root dirty", func(t *testing.T) {
+		t.Parallel()
+		require := require.New(t)
 
-// 	fs, err = defaultFactories[".rar"](&Dummy{}, nil)
-// 	require.NoError(err)
-// 	require.NotNil(fs)
+		fsPath, nestedFs, nestedFsPath, err := resolver.resolvePath("//.//", func(path string) (File, error) {
+			require.Equal("/", path)
+			return &Dummy{}, nil
+		})
+		require.Nil(err)
+		require.Nil(nestedFs)
+		require.Equal("/", fsPath)
+		require.Equal("", nestedFsPath)
+	})
+	t.Run("fs dirty", func(t *testing.T) {
+		t.Parallel()
+		require := require.New(t)
 
-// 	fs, err = defaultFactories[".7z"](&Dummy{}, nil)
-// 	require.NoError(err)
-// 	require.NotNil(fs)
-// }
+		fsPath, nestedFs, nestedFsPath, err := resolver.resolvePath("//.//f1.rar", func(path string) (File, error) {
+			require.Equal("/f1.rar", path)
+			return &Dummy{}, nil
+		})
+		require.Nil(err)
+		require.Equal("/f1.rar", fsPath)
+		require.Equal("/", nestedFsPath)
+		require.IsType(&archive{}, nestedFs)
+	})
+	t.Run("inside folder", func(t *testing.T) {
+		t.Parallel()
+		require := require.New(t)
 
-// func TestStorageAddFs(t *testing.T) {
-// 	t.Parallel()
+		fsPath, nestedFs, nestedFsPath, err := resolver.resolvePath("//test1/f1.rar", func(path string) (File, error) {
+			require.Equal("/test1/f1.rar", path)
+			return &Dummy{}, nil
+		})
+		require.Nil(err)
+		require.IsType(&archive{}, nestedFs)
+		require.Equal("/test1/f1.rar", fsPath)
+		require.Equal("/", nestedFsPath)
+	})
+}
 
-// 	require := require.New(t)
+func TestArchiveFactories(t *testing.T) {
+	t.Parallel()
 
-// 	s := newStorage(dummyFactories)
+	require := require.New(t)
 
-// 	err := s.AddFS(&DummyFs{}, "/test")
-// 	require.NoError(err)
+	require.Contains(ArchiveFactories, ".zip")
+	require.Contains(ArchiveFactories, ".rar")
+	require.Contains(ArchiveFactories, ".7z")
 
-// 	f, err := s.Get("/test/dir/here/file1.txt")
-// 	require.NoError(err)
-// 	require.NotNil(f)
+	fs, err := ArchiveFactories[".zip"](&Dummy{})
+	require.NoError(err)
+	require.NotNil(fs)
 
-// 	err = s.AddFS(&DummyFs{}, "/test")
-// 	require.Error(err)
-// }
+	fs, err = ArchiveFactories[".rar"](&Dummy{})
+	require.NoError(err)
+	require.NotNil(fs)
 
-// func TestStorageWindowsPath(t *testing.T) {
-// 	t.Parallel()
+	fs, err = ArchiveFactories[".7z"](&Dummy{})
+	require.NoError(err)
+	require.NotNil(fs)
+}
 
-// 	require := require.New(t)
+func TestFiles(t *testing.T) {
+	t.Parallel()
+	require := require.New(t)
 
-// 	s := newStorage(dummyFactories)
+	files := map[string]*Dummy{
+		"/test/file.txt": &Dummy{},
+	}
+	{
+		file, err := getFile(files, "/test")
+		require.Nil(err)
+		require.Equal(&Dir{}, file)
+	}
+	{
+		file, err := getFile(files, "/test/file.txt")
+		require.Nil(err)
+		require.Equal(&Dummy{}, file)
+	}
 
-// 	err := s.Add(&Dummy{}, "\\path\\to\\dummy\\file.txt")
-// 	require.NoError(err)
-
-// 	file, err := s.Get("\\path\\to\\dummy\\file.txt")
-// 	require.NoError(err)
-// 	require.Equal(&Dummy{}, file)
-
-// 	file, err = s.Get("/path/to/dummy/file.txt")
-// 	require.NoError(err)
-// 	require.Equal(&Dummy{}, file)
-// }
-
-// var dummyFactories = map[string]vfs.FsFactory{
-// 	".test": func(f vfs.File, factories map[string]vfs.FsFactory) (vfs.Filesystem, error) {
-// 		return &DummyFs{}, nil
-// 	},
-// }
-
-// func TestStorage(t *testing.T) {
-// 	t.Parallel()
-
-// 	require := require.New(t)
-
-// 	s := newStorage(dummyFactories)
-
-// 	err := s.Add(&Dummy{}, "/path/to/dummy/file.txt")
-// 	require.NoError(err)
-
-// 	err = s.Add(&Dummy{}, "/path/to/dummy/file2.txt")
-// 	require.NoError(err)
-
-// 	contains := s.Has("/path")
-// 	require.True(contains)
-
-// 	contains = s.Has("/path/to/dummy/")
-// 	require.True(contains)
-
-// 	file, err := s.Get("/path/to/dummy/file.txt")
-// 	require.NoError(err)
-// 	require.Equal(&Dummy{}, file)
-
-// 	file, err = s.Get("/path/to/dummy/file3.txt")
-// 	require.Error(err)
-// 	require.Nil(file)
-
-// 	files, err := s.Children("/path/to/dummy/")
-// 	require.NoError(err)
-// 	require.Len(files, 2)
-// 	require.Contains(files, "file.txt")
-// 	require.Contains(files, "file2.txt")
-
-// 	err = s.Add(&Dummy{}, "/path/to/dummy/folder/file.txt")
-// 	require.NoError(err)
-
-// 	files, err = s.Children("/path/to/dummy/")
-// 	require.NoError(err)
-// 	require.Len(files, 3)
-// 	require.Contains(files, "file.txt")
-// 	require.Contains(files, "file2.txt")
-// 	require.Contains(files, "folder")
-
-// 	err = s.Add(&Dummy{}, "path/file4.txt")
-// 	require.NoError(err)
-
-// 	require.True(s.Has("/path/file4.txt"))
-
-// 	files, err = s.Children("/")
-// 	require.NoError(err)
-// 	require.Len(files, 1)
-
-// 	err = s.Add(&Dummy{}, "/path/special_file.test")
-// 	require.NoError(err)
-
-// 	file, err = s.Get("/path/special_file.test/dir/here/file1.txt")
-// 	require.NoError(err)
-// 	require.Equal(&Dummy{}, file)
-
-// 	files, err = s.Children("/path/special_file.test")
-// 	require.NoError(err)
-// 	require.NotNil(files)
-
-// 	files, err = s.Children("/path/special_file.test/dir/here")
-// 	require.NoError(err)
-// 	require.Len(files, 2)
-
-// 	err = s.Add(&Dummy{}, "/path/to/__special__path/file3.txt")
-// 	require.NoError(err)
-
-// 	file, err = s.Get("/path/to/__special__path/file3.txt")
-// 	require.NoError(err)
-// 	require.Equal(&Dummy{}, file)
-
-// 	s.Clear()
-// }
+	{
+		out, err := listFilesInDir(files, "/test")
+		require.Nil(err)
+		require.Contains(out, "file.txt")
+		require.Equal(&Dummy{}, out["file.txt"])
+	}
+}
diff --git a/src/host/vfs/torrent.go b/src/host/vfs/torrent.go
index 99b2af1..2089f03 100644
--- a/src/host/vfs/torrent.go
+++ b/src/host/vfs/torrent.go
@@ -33,7 +33,11 @@ func (fs *TorrentFs) files() map[string]*torrentFile {
 	files := make(map[string]*torrentFile)
 	<-fs.t.GotInfo()
 	for _, file := range fs.t.Files() {
-		p := Clean(file.Path())
+		if file.Priority() == torrent.PiecePriorityNone {
+			continue
+		}
+
+		p := clean(file.Path())
 		files[p] = &torrentFile{
 			readerFunc: file.NewReader,
 			len:        file.Length(),
@@ -73,6 +77,12 @@ func (fs *TorrentFs) ReadDir(name string) (map[string]File, error) {
 	return listFilesInDir(fs.files(), fsPath)
 }
 
+func (fs *TorrentFs) Unlink(name string) error {
+	file := fs.t.Files()[0]
+	file.SetPriority(torrent.PiecePriorityNone)
+	return nil
+}
+
 type reader interface {
 	iio.Reader
 	missinggo.ReadContexter
@@ -111,18 +121,11 @@ func readAtLeast(r missinggo.ReadContexter, timeout int, buf []byte, min int) (n
 	for n < min && err == nil {
 		var nn int
 
-		ctx, cancel := context.WithCancel(context.Background())
-		timer := time.AfterFunc(
-			time.Duration(timeout)*time.Second,
-			func() {
-				cancel()
-			},
-		)
+		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
+		defer cancel()
 
 		nn, err = r.ReadContext(ctx, buf[n:])
 		n += nn
-
-		timer.Stop()
 	}
 	if n >= min {
 		err = nil
@@ -175,15 +178,8 @@ func (d *torrentFile) Close() error {
 
 func (d *torrentFile) Read(p []byte) (n int, err error) {
 	d.load()
-	ctx, cancel := context.WithCancel(context.Background())
-	timer := time.AfterFunc(
-		time.Duration(d.timeout)*time.Second,
-		func() {
-			cancel()
-		},
-	)
-
-	defer timer.Stop()
+	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(d.timeout)*time.Second)
+	defer cancel()
 
 	return d.reader.ReadContext(ctx, p)
 }
diff --git a/src/host/vfs/torrent_test.go b/src/host/vfs/torrent_test.go
index 5f5c3cc..bb9fdb0 100644
--- a/src/host/vfs/torrent_test.go
+++ b/src/host/vfs/torrent_test.go
@@ -85,6 +85,8 @@ func TestMain(m *testing.M) {
 // }
 
 func TestReadAtTorrent(t *testing.T) {
+	t.Parallel()
+
 	require := require.New(t)
 
 	to, err := Cli.AddMagnet(testMagnet)
diff --git a/src/host/vfs/utils.go b/src/host/vfs/utils.go
index d4b2d77..7f36933 100644
--- a/src/host/vfs/utils.go
+++ b/src/host/vfs/utils.go
@@ -1,55 +1,14 @@
 package vfs
 
 import (
-	"io/fs"
 	"path"
 	"strings"
 )
 
-var ErrNotExist = fs.ErrNotExist
-
-func getFile[F File](m map[string]F, name string) (File, error) {
-	name = Clean(name)
-	if name == Separator {
-		return &Dir{}, nil
-	}
-
-	f, ok := m[name]
-	if ok {
-		return f, nil
-	}
-
-	for p := range m {
-		if strings.HasPrefix(p, name) {
-			return &Dir{}, nil
-		}
-	}
-
-	return nil, ErrNotExist
-}
-
-func listFilesInDir[F File](m map[string]F, name string) (map[string]File, error) {
-	name = Clean(name)
-
-	out := map[string]File{}
-	for p, f := range m {
-		if strings.HasPrefix(p, name) {
-			parts := strings.Split(trimRelPath(p, name), Separator)
-			if len(parts) == 1 {
-				out[parts[0]] = f
-			} else {
-				out[parts[0]] = &Dir{}
-			}
-		}
-	}
-
-	return out, nil
-}
-
 func trimRelPath(p, t string) string {
 	return strings.Trim(strings.TrimPrefix(p, t), "/")
 }
 
-func Clean(p string) string {
+func clean(p string) string {
 	return path.Clean(Separator + strings.ReplaceAll(p, "\\", "/"))
 }
diff --git a/src/mounts/fuse/handler.go b/src/mounts/fuse/handler.go
index c3b4af9..867e466 100644
--- a/src/mounts/fuse/handler.go
+++ b/src/mounts/fuse/handler.go
@@ -1,3 +1,5 @@
+//go:build cgo
+
 package fuse
 
 import (
@@ -38,7 +40,7 @@ func (s *Handler) Mount(vfs vfs.Filesystem) error {
 		}
 	}
 
-	host := fuse.NewFileSystemHost(NewFS(vfs))
+	host := fuse.NewFileSystemHost(newFuseFS(vfs))
 
 	// TODO improve error handling here
 	go func() {
diff --git a/src/mounts/fuse/handler_nocgo.go b/src/mounts/fuse/handler_nocgo.go
new file mode 100644
index 0000000..4e0e92e
--- /dev/null
+++ b/src/mounts/fuse/handler_nocgo.go
@@ -0,0 +1,23 @@
+//go:build !cgo
+
+package fuse
+
+import (
+	"fmt"
+
+	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
+)
+
+type Handler struct{}
+
+func NewHandler(fuseAllowOther bool, path string) *Handler {
+	return &Handler{}
+}
+
+func (s *Handler) Mount(vfs vfs.Filesystem) error {
+	return fmt.Errorf("tstor was build without fuse support")
+
+}
+
+func (s *Handler) Unmount() {
+}
diff --git a/src/mounts/fuse/mount.go b/src/mounts/fuse/mount.go
index e57a6f2..da6a390 100644
--- a/src/mounts/fuse/mount.go
+++ b/src/mounts/fuse/mount.go
@@ -1,3 +1,5 @@
+//go:build cgo
+
 package fuse
 
 import (
@@ -14,22 +16,22 @@ import (
 	"github.com/rs/zerolog/log"
 )
 
-type FS struct {
+type fuseFS struct {
 	fuse.FileSystemBase
 	fh *fileHandler
 
 	log zerolog.Logger
 }
 
-func NewFS(fs vfs.Filesystem) fuse.FileSystemInterface {
+func newFuseFS(fs vfs.Filesystem) fuse.FileSystemInterface {
 	l := log.Logger.With().Str("component", "fuse").Logger()
-	return &FS{
+	return &fuseFS{
 		fh:  &fileHandler{fs: fs},
 		log: l,
 	}
 }
 
-func (fs *FS) Open(path string, flags int) (errc int, fh uint64) {
+func (fs *fuseFS) Open(path string, flags int) (errc int, fh uint64) {
 	fh, err := fs.fh.OpenHolder(path)
 	if os.IsNotExist(err) {
 		fs.log.Debug().Str("path", path).Msg("file does not exists")
@@ -46,15 +48,15 @@ func (fs *FS) Open(path string, flags int) (errc int, fh uint64) {
 
 // Unlink removes a file.
 // The FileSystemBase implementation returns -ENOSYS.
-func (fs *FS) Unlink(path string) int {
+func (fs *fuseFS) Unlink(path string) int {
 	return -fuse.ENOSYS
 }
 
-func (fs *FS) Opendir(path string) (errc int, fh uint64) {
+func (fs *fuseFS) Opendir(path string) (errc int, fh uint64) {
 	return fs.Open(path, 0)
 }
 
-func (fs *FS) Getattr(path string, stat *fuse.Stat_t, fh uint64) (errc int) {
+func (fs *fuseFS) Getattr(path string, stat *fuse.Stat_t, fh uint64) (errc int) {
 	if path == "/" {
 		stat.Mode = fuse.S_IFDIR | 0555
 		return 0
@@ -81,7 +83,7 @@ func (fs *FS) Getattr(path string, stat *fuse.Stat_t, fh uint64) (errc int) {
 	return 0
 }
 
-func (fs *FS) Read(path string, dest []byte, off int64, fh uint64) int {
+func (fs *fuseFS) Read(path string, dest []byte, off int64, fh uint64) int {
 	file, err := fs.fh.GetFile(path, fh)
 	if os.IsNotExist(err) {
 		fs.log.Error().Err(err).Str("path", path).Msg("file not found on READ operation")
@@ -110,7 +112,7 @@ func (fs *FS) Read(path string, dest []byte, off int64, fh uint64) int {
 	return n
 }
 
-func (fs *FS) Release(path string, fh uint64) int {
+func (fs *fuseFS) Release(path string, fh uint64) int {
 	if err := fs.fh.Remove(fh); err != nil {
 		fs.log.Error().Err(err).Str("path", path).Msg("error getting holder when releasing file")
 		return -fuse.EIO
@@ -119,11 +121,11 @@ func (fs *FS) Release(path string, fh uint64) int {
 	return 0
 }
 
-func (fs *FS) Releasedir(path string, fh uint64) int {
+func (fs *fuseFS) Releasedir(path string, fh uint64) int {
 	return fs.Release(path, fh)
 }
 
-func (fs *FS) Readdir(path string,
+func (fs *fuseFS) Readdir(path string,
 	fill func(name string, stat *fuse.Stat_t, ofst int64) bool,
 	ofst int64,
 	fh uint64) (errc int) {
diff --git a/src/mounts/fuse/mount_test.go b/src/mounts/fuse/mount_test.go
index 0717955..3d16ca4 100644
--- a/src/mounts/fuse/mount_test.go
+++ b/src/mounts/fuse/mount_test.go
@@ -1,3 +1,5 @@
+//go:build cgo
+
 package fuse
 
 import (

From 2b39afca3bc942761160f6f48187fb4572ed25a9 Mon Sep 17 00:00:00 2001
From: royalcat <k.adamovich20@gmail.com>
Date: Wed, 18 Oct 2023 14:07:19 +0300
Subject: [PATCH 02/18] update readme

---
 .github/FUNDING.yml | 12 ------
 README.md           | 91 ++++-----------------------------------------
 2 files changed, 8 insertions(+), 95 deletions(-)
 delete mode 100644 .github/FUNDING.yml

diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
deleted file mode 100644
index 46a25bc..0000000
--- a/.github/FUNDING.yml
+++ /dev/null
@@ -1,12 +0,0 @@
-# These are supported funding model platforms
-
-github: [ajnavarro]
-# patreon: # Replace with a single Patreon username
-# open_collective: # Replace with a single Open Collective username
-# ko_fi: # Replace with a single Ko-fi username
-# tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
-# community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
-# liberapay: # Replace with a single Liberapay username
-# issuehunt: # Replace with a single IssueHunt username
-# otechie: # Replace with a single Otechie username
-# custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
diff --git a/README.md b/README.md
index 795d95a..9a7f163 100644
--- a/README.md
+++ b/README.md
@@ -1,93 +1,18 @@
-[![Releases][releases-shield]][releases-url]
-[![Contributors][contributors-shield]][contributors-url]
-[![Forks][forks-shield]][forks-url]
-[![Stargazers][stars-shield]][stars-url]
-[![Issues][issues-shield]][issues-url]
-[![GPL3 License][license-shield]][license-url]
-[![Coveralls][coveralls-shield]][coveralls-url]
-[![Docker Image][docker-pulls-shield]][docker-pulls-url]
+# tstor (WIP)
 
-<!-- PROJECT LOGO -->
-<br />
-<p align="center">
-  <a href="https://git.kmsign.ru/royalcat/tstor">
-    <img src="mkdocs/docs/images/tstor_icon.png" alt="Logo" width="100">
-  </a>
+tstor is an advanced remote torrent clien for self-hosting enthusiasts.
 
-  <h3 align="center">tstor</h3>
+It expose virtual filesystem with torrents and archives presented as fully featured directories with limited amount of mutability. Virtual filesystem can be exported as a webDAV, HTTP endpoint or NFS(WIP).
 
-  <p align="center">
-    Torrent client with on-demand file downloading as a filesystem.
-    <br />
-    <br />
-    <a href="https://git.kmsign.ru/royalcat/tstor/issues">Report a Bug</a>
-    ¡
-    <a href="https://git.kmsign.ru/royalcat/tstor/issues">Request Feature</a>
-  </p>
-</p>
-
-## About The Project
-
-![tstor Screen Shot][product-screenshot]
-
-tstor is an alternative torrent client.
-It can expose torrent files as a standard FUSE, webDAV or HTTP endpoint and download them on demand, allowing random reads using a fixed amount of disk space.
-
-tstor tries to make easier integrations with other applications using torrent files, presenting them as a standard filesystem.
-
-**Note that tstor is in beta version, it is a proof of concept with a lot of bugs.**
-
-## Use Cases
-
-- Play **multimedia files** on your favorite video or audio player. These files will be downloaded on demand and only the needed parts.
-- Explore TBs of data from public **datasets** only downloading the parts you need. Use **Jupyter Notebooks** directly to process or analyze this data.
-- Give access to your latest dataset creation just by sharing a magnet link. People will start using your data in seconds.
-- Play your **ROM backups** directly from the torrent file. You can have virtually GBs in games and only downloaded the needed ones.
-
-## Documentation
-
-Check [here][main-url] for further documentation.
-
-## Contributing
-
-Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
-
-Some areas need more care than others:
-
-- Windows and macOS tests and compatibility. I don't have any easy way to test tstor on these operating systems.
-- Web interface. Web development is not my _forte_.
-- Tutorials. Share with the community your use case!
+tstor is based on amazing [distribyted](https://github.com/distribyted/distribyted), but has more focus on store a torrent data when streaming it.
 
 ## Special thanks
 
-- [Anacrolix BitTorrent client package and utilities][torrent-repo-url]. An amazing torrent library with file seek support.
-- [Nwaples rardecode library, experimental branch][rardecode-repo-url]. The only go library that is able to seek over rar files and avoid to use `io.Discard`.
-- [Bodgit 7zip library][sevenzip-repo-url]. Amazing library to decode 7zip files.
+- [distribyted](https://github.com/distribyted/distribyted)
+- [Anacrolix BitTorrent client package and utilities](https://github.com/anacrolix/torrent-repo-url). An amazing torrent library with file seek support.
+- [Nwaples rardecode library, experimental branch](https://github.com/nwaples/rardecode/tree/experimental). The only go library that is able to seek over rar files and avoid to use `io.Discard`.
+- [Bodgit 7zip library](https://github.com/bodgit/sevenzip). Amazing library to decode 7zip files.
 
 ## License
 
 Distributed under the GPL3 license. See `LICENSE` for more information.
-
-[sevenzip-repo-url]: https://github.com/bodgit/sevenzip
-[rardecode-repo-url]: https://github.com/nwaples/rardecode/tree/experimental
-[torrent-repo-url]: https://github.com/anacrolix/torrent
-[main-url]: https://tstor.com
-[releases-shield]: https://img.shields.io/github/v/release/tstor/tstor.svg?style=flat-square
-[releases-url]: https://git.kmsign.ru/royalcat/tstor/releases
-[docker-pulls-shield]: https://img.shields.io/docker/pulls/tstor/tstor.svg?style=flat-square
-[docker-pulls-url]: https://hub.docker.com/r/tstor/tstor
-[contributors-shield]: https://img.shields.io/github/contributors/tstor/tstor.svg?style=flat-square
-[contributors-url]: https://git.kmsign.ru/royalcat/tstor/graphs/contributors
-[forks-shield]: https://img.shields.io/github/forks/tstor/tstor.svg?style=flat-square
-[forks-url]: https://git.kmsign.ru/royalcat/tstor/network/members
-[stars-shield]: https://img.shields.io/github/stars/tstor/tstor.svg?style=flat-square
-[stars-url]: https://git.kmsign.ru/royalcat/tstor/stargazers
-[issues-shield]: https://img.shields.io/github/issues/tstor/tstor.svg?style=flat-square
-[issues-url]: https://git.kmsign.ru/royalcat/tstor/issues
-[releases-url]: https://git.kmsign.ru/royalcat/tstor/releases
-[license-shield]: https://img.shields.io/github/license/tstor/tstor.svg?style=flat-square
-[license-url]: https://git.kmsign.ru/royalcat/tstor/blob/master/LICENSE
-[product-screenshot]: mkdocs/docs/images/tstor.gif
-[example-config]: https://git.kmsign.ru/royalcat/tstor/blob/master/examples/conf_example.yaml
-[coveralls-shield]: https://img.shields.io/coveralls/github/tstor/tstor?style=flat-square
-[coveralls-url]: https://coveralls.io/github/tstor/tstor

From 0350ecba9a7f84cd28a5816e621054bf81dbeb85 Mon Sep 17 00:00:00 2001
From: royalcat <k.adamovich20@gmail.com>
Date: Fri, 22 Dec 2023 02:15:39 +0300
Subject: [PATCH 03/18] oprimized, working

---
 .gitignore                           |   3 +-
 .golangci.yml                        |  52 ++++
 .vscode/launch.json                  |   2 +-
 .vscode/settings.json                |   5 -
 cmd/tstor/main.go                    | 186 +++++-------
 go.mod                               | 154 +++++-----
 go.sum                               | 430 ++++++++++++++++-----------
 src/config/default.go                |   4 +
 src/config/load.go                   |   5 +-
 src/config/model.go                  |   6 +
 src/host/storage.go                  | 105 +------
 src/host/torrent/client.go           |  16 +-
 src/host/torrent/piece-completion.go | 131 ++++++++
 src/host/torrent/service.go          | 204 ++-----------
 src/host/torrent/storage.go          | 306 +++++++++++++++++++
 src/host/vfs/archive.go              |  51 +++-
 src/host/vfs/archive_test.go         |  11 +-
 src/host/vfs/dir.go                  |  31 +-
 src/host/vfs/fs.go                   |  52 +++-
 src/host/vfs/fs_test.go              |  31 +-
 src/host/vfs/memory.go               |  23 +-
 src/host/vfs/memory_test.go          |   4 +-
 src/host/vfs/os.go                   |  67 +++--
 src/host/vfs/resolver.go             |  82 +++--
 src/host/vfs/resolver_test.go        |  69 +++--
 src/host/vfs/torrent.go              |  78 +++--
 src/host/vfs/utils.go                |  23 +-
 src/http/http.go                     |   2 +
 src/iio/wrapper_test.go              |   2 +-
 src/mounts/fuse/mount.go             |   6 +-
 src/mounts/fuse/mount_test.go        |   4 +-
 src/mounts/httpfs/httpfs.go          |  54 ++--
 src/mounts/nfs/handler.go            |  16 +
 src/mounts/nfs/wrapper-v3.go         | 190 ++++++++++++
 src/mounts/nfs/wrapper-v4.go         | 164 ++++++++++
 src/mounts/webdav/fs.go              |  38 +--
 src/mounts/webdav/fs_test.go         |  11 +-
 src/mounts/webdav/http.go            |  17 +-
 38 files changed, 1809 insertions(+), 826 deletions(-)
 create mode 100644 .golangci.yml
 delete mode 100644 .vscode/settings.json
 create mode 100644 src/host/torrent/piece-completion.go
 create mode 100644 src/host/torrent/storage.go
 create mode 100644 src/mounts/nfs/handler.go
 create mode 100644 src/mounts/nfs/wrapper-v3.go
 create mode 100644 src/mounts/nfs/wrapper-v4.go

diff --git a/.gitignore b/.gitignore
index d5f5981..fcc7b9b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,5 @@ tstor-data
 httpfs_vfsdata.go
 bin/
 coverage.out
-bin
\ No newline at end of file
+bin
+build
\ No newline at end of file
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 0000000..d7760dc
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,52 @@
+linters:
+  enable:
+    - revive
+    - exhaustruct
+    - nakedret
+    - gomoddirectives
+    - importas
+    - misspell
+    - promlinter
+    - prealloc
+    - predeclared
+    - stylecheck
+    - ineffassign
+    - dupl
+    - govet
+    - staticcheck
+    - unused
+    - asasalint
+    - asciicheck
+    - bidichk
+    - bodyclose
+    # - containedctx
+    - durationcheck
+    - errcheck
+    - nakedret
+    - testifylint
+
+linters-settings:
+  revive:
+    ignore-generated-header: true
+    severity: warning
+    rules:
+      - name: blank-imports
+        disabled: true
+  staticcheck:
+    checks:
+      - "-SA4006"
+  gosimple:
+    checks:
+      - "-S1002"
+  exhaustruct:
+    include:
+      - ".*Service"
+      - ".*Server.*"
+    exclude:
+      - ".*mutex"
+      - ".*mutex"
+  stylecheck:
+    checks:
+      - "-ST1003"
+  dupl:
+    threshold: 180
diff --git a/.vscode/launch.json b/.vscode/launch.json
index b9d624c..4b9ec0e 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -10,7 +10,7 @@
             "request": "launch",
             "mode": "auto",
             "program": "${workspaceFolder}/cmd/tstor/main.go",
-            "cwd": "${workspaceFolder}/bin",
+            "cwd": "${workspaceFolder}/bin"
         }
     ]
 }
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
deleted file mode 100644
index e40c94c..0000000
--- a/.vscode/settings.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
-    "yaml.schemas": {
-        "https://json.schemastore.org/github-workflow.json": "file:///home/royalcat/projects/distribyted/.github/workflows/mkdocs.yml"
-    }
-}
\ No newline at end of file
diff --git a/cmd/tstor/main.go b/cmd/tstor/main.go
index ca84dab..eddba22 100644
--- a/cmd/tstor/main.go
+++ b/cmd/tstor/main.go
@@ -1,27 +1,30 @@
 package main
 
 import (
-	"bufio"
 	"fmt"
+
+	"net"
+	nethttp "net/http"
+	_ "net/http/pprof"
 	"os"
 	"os/signal"
 	"path/filepath"
-	"runtime"
 	"syscall"
 	"time"
 
 	"git.kmsign.ru/royalcat/tstor/src/config"
 	"git.kmsign.ru/royalcat/tstor/src/host"
 	"git.kmsign.ru/royalcat/tstor/src/host/torrent"
-	"github.com/anacrolix/torrent/storage"
-	"github.com/gin-gonic/gin"
+	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
 	"github.com/rs/zerolog/log"
 	"github.com/urfave/cli/v2"
+	wnfs "github.com/willscott/go-nfs"
 
 	"git.kmsign.ru/royalcat/tstor/src/http"
 	dlog "git.kmsign.ru/royalcat/tstor/src/log"
 	"git.kmsign.ru/royalcat/tstor/src/mounts/fuse"
 	"git.kmsign.ru/royalcat/tstor/src/mounts/httpfs"
+	"git.kmsign.ru/royalcat/tstor/src/mounts/nfs"
 	"git.kmsign.ru/royalcat/tstor/src/mounts/webdav"
 )
 
@@ -44,16 +47,7 @@ func main() {
 		},
 
 		Action: func(c *cli.Context) error {
-			err := load(c.String(configFlag))
-
-			// stop program execution on errors to avoid flashing consoles
-			if err != nil && runtime.GOOS == "windows" {
-				log.Error().Err(err).Msg("problem starting application")
-				fmt.Print("Press 'Enter' to continue...")
-				bufio.NewReader(os.Stdin).ReadBytes('\n')
-			}
-
-			return err
+			return run(c.String(configFlag))
 		},
 
 		HideHelpCommand: true,
@@ -64,50 +58,8 @@ func main() {
 	}
 }
 
-func setupStorage(tcfg config.TorrentClient) (storage.ClientImplCloser, storage.PieceCompletion, error) {
-	pcp := filepath.Join(tcfg.DataFolder, "piece-completion")
-	if err := os.MkdirAll(pcp, 0744); err != nil {
-		return nil, nil, fmt.Errorf("error creating piece completion folder: %w", err)
-	}
-	pc, err := storage.NewBoltPieceCompletion(pcp)
-	if err != nil {
-		return nil, nil, fmt.Errorf("error creating servers piece completion: %w", err)
-	}
+func run(configPath string) error {
 
-	// TODO implement cache/storage switching
-	// cacheDir := filepath.Join(tcfg.DataFolder, "cache")
-	// if err := os.MkdirAll(cacheDir, 0744); err != nil {
-	// 	return nil, nil, fmt.Errorf("error creating piece completion folder: %w", err)
-	// }
-	// fc, err := filecache.NewCache(cacheDir)
-	// if err != nil {
-	// 	return nil, nil, fmt.Errorf("error creating cache: %w", err)
-	// }
-	// log.Info().Msg(fmt.Sprintf("setting cache size to %d MB", 1024))
-	// fc.SetCapacity(1024 * 1024 * 1024)
-
-	// rp := storage.NewResourcePieces(fc.AsResourceProvider())
-	// st := &stc{rp}
-
-	filesDir := filepath.Join(tcfg.DataFolder, "files")
-	if err := os.MkdirAll(pcp, 0744); err != nil {
-		return nil, nil, fmt.Errorf("error creating piece completion folder: %w", err)
-	}
-
-	st := storage.NewFileWithCompletion(filesDir, pc)
-
-	return st, pc, nil
-}
-
-type stc struct {
-	storage.ClientImpl
-}
-
-func (s *stc) Close() error {
-	return nil
-}
-
-func load(configPath string) error {
 	conf, err := config.Load(configPath)
 	if err != nil {
 		return fmt.Errorf("error loading configuration: %w", err)
@@ -115,6 +67,11 @@ func load(configPath string) error {
 
 	dlog.Load(&conf.Log)
 
+	err = syscall.Setpriority(syscall.PRIO_PGRP, 0, 19)
+	if err != nil {
+		log.Err(err).Msg("set priority failed")
+	}
+
 	if err := os.MkdirAll(conf.TorrentClient.MetadataFolder, 0744); err != nil {
 		return fmt.Errorf("error creating metadata folder: %w", err)
 	}
@@ -123,22 +80,25 @@ func load(configPath string) error {
 	if err != nil {
 		return fmt.Errorf("error starting item store: %w", err)
 	}
+	defer fis.Close()
 
 	id, err := torrent.GetOrCreatePeerID(filepath.Join(conf.TorrentClient.MetadataFolder, "ID"))
 	if err != nil {
 		return fmt.Errorf("error creating node ID: %w", err)
 	}
 
-	st, _, err := setupStorage(conf.TorrentClient)
+	st, _, err := torrent.SetupStorage(conf.TorrentClient)
 	if err != nil {
 		return err
 	}
+	defer st.Close()
 
 	c, err := torrent.NewClient(st, fis, &conf.TorrentClient, id)
 	if err != nil {
 		return fmt.Errorf("error starting torrent client: %w", err)
 	}
 	c.AddDhtNodes(conf.TorrentClient.DHTNodes)
+	defer c.Close()
 
 	ts := torrent.NewService(c, conf.TorrentClient.AddTimeout, conf.TorrentClient.ReadTimeout)
 
@@ -147,46 +107,15 @@ func load(configPath string) error {
 	}
 	cfs := host.NewStorage(conf.DataFolder, ts)
 
-	var mh *fuse.Handler
 	if conf.Mounts.Fuse.Enabled {
-		mh = fuse.NewHandler(conf.Mounts.Fuse.AllowOther, conf.Mounts.Fuse.Path)
+		mh := fuse.NewHandler(conf.Mounts.Fuse.AllowOther, conf.Mounts.Fuse.Path)
+		err := mh.Mount(cfs)
+		if err != nil {
+			return fmt.Errorf("mount fuse error: %w", err)
+		}
+		defer mh.Unmount()
 	}
 
-	sigChan := make(chan os.Signal)
-	signal.Notify(sigChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
-
-	go func() {
-
-		<-sigChan
-		log.Info().Msg("closing servers...")
-		// for _, s := range servers {
-		// 	if err := s.Close(); err != nil {
-		// 		log.Warn().Err(err).Msg("problem closing server")
-		// 	}
-		// }
-		log.Info().Msg("closing items database...")
-		fis.Close()
-		log.Info().Msg("closing torrent client...")
-		c.Close()
-		if mh != nil {
-			log.Info().Msg("unmounting fuse filesystem...")
-			mh.Unmount()
-		}
-
-		log.Info().Msg("exiting")
-		os.Exit(1)
-	}()
-
-	go func() {
-		if mh == nil {
-			return
-		}
-
-		if err := mh.Mount(cfs); err != nil {
-			log.Info().Err(err).Msg("error mounting filesystems")
-		}
-	}()
-
 	if conf.Mounts.WebDAV.Enabled {
 		go func() {
 			if err := webdav.NewWebDAVServer(cfs, conf.Mounts.WebDAV.Port, conf.Mounts.WebDAV.User, conf.Mounts.WebDAV.Pass); err != nil {
@@ -199,24 +128,63 @@ func load(configPath string) error {
 	if conf.Mounts.HttpFs.Enabled {
 		go func() {
 			httpfs := httpfs.NewHTTPFS(cfs)
-
-			r := gin.New()
-
-			r.GET("*filepath", func(c *gin.Context) {
-				path := c.Param("filepath")
-				c.FileFromFS(path, httpfs)
-			})
-
-			log.Info().Str("host", fmt.Sprintf("0.0.0.0:%d", conf.Mounts.HttpFs.Port)).Msg("starting HTTPFS")
-			if err := r.Run(fmt.Sprintf("0.0.0.0:%d", conf.Mounts.HttpFs.Port)); err != nil {
+			err = nethttp.ListenAndServe(fmt.Sprintf("0.0.0.0:%d", conf.Mounts.HttpFs.Port), nethttp.FileServer(httpfs))
+			if err != nil {
 				log.Error().Err(err).Msg("error starting HTTPFS")
 			}
+			// r := gin.New()
+
+			// r.GET("*filepath", func(c *gin.Context) {
+			// 	path := c.Param("filepath")
+			// 	c.FileFromFS(path, httpfs)
+			// })
+
+			// log.Info().Str("host", fmt.Sprintf("0.0.0.0:%d", conf.Mounts.HttpFs.Port)).Msg("starting HTTPFS")
+			// if err := r.Run(fmt.Sprintf("0.0.0.0:%d", conf.Mounts.HttpFs.Port)); err != nil {
+			// 	log.Error().Err(err).Msg("error starting HTTPFS")
+			// }
 		}()
 	}
 
-	logFilename := filepath.Join(conf.Log.Path, dlog.FileName)
+	if conf.Mounts.NFS.Enabled {
+		go func() {
+			listener, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", conf.Mounts.NFS.Port))
+			panicOnErr(err, "starting TCP listener")
+			log.Info().Str("host", listener.Addr().String()).Msg("starting NFS server")
+			handler, err := nfs.NewNFSv3Handler(cfs)
+			panicOnErr(err, "creating NFS handler")
+			panicOnErr(wnfs.Serve(listener, handler), "serving nfs")
+		}()
+	}
 
-	err = http.New(nil, nil, ts, logFilename, conf)
-	log.Error().Err(err).Msg("error initializing HTTP server")
-	return err
+	dataFS := vfs.NewOsFs(conf.DataFolder)
+
+	go func() {
+		if err := webdav.NewWebDAVServer(dataFS, 36912, conf.Mounts.WebDAV.User, conf.Mounts.WebDAV.Pass); err != nil {
+			log.Error().Err(err).Msg("error starting webDAV")
+		}
+
+		log.Warn().Msg("webDAV configuration not found!")
+	}()
+
+	go func() {
+		logFilename := filepath.Join(conf.Log.Path, dlog.FileName)
+
+		err = http.New(nil, nil, ts, logFilename, conf)
+		log.Error().Err(err).Msg("error initializing HTTP server")
+	}()
+
+	sigChan := make(chan os.Signal, 1)
+	signal.Notify(sigChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
+	<-sigChan
+
+	return nil
+}
+
+func panicOnErr(err error, desc string) {
+	if err == nil {
+		return
+	}
+	log.Err(err).Msg(desc)
+	log.Panic()
 }
diff --git a/go.mod b/go.mod
index 421dc8d..a9b95cd 100644
--- a/go.mod
+++ b/go.mod
@@ -3,14 +3,18 @@ module git.kmsign.ru/royalcat/tstor
 go 1.21
 
 require (
-	github.com/anacrolix/dht/v2 v2.20.0
-	github.com/anacrolix/log v0.14.3-0.20230823030427-4b296d71a6b4
-	github.com/anacrolix/missinggo/v2 v2.7.2
-	github.com/anacrolix/torrent v1.52.6-0.20230929044811-45c91b322ad1
+	github.com/anacrolix/dht/v2 v2.21.0
+	github.com/anacrolix/log v0.14.5
+	github.com/anacrolix/missinggo v1.3.0
+	github.com/anacrolix/missinggo/v2 v2.7.3
+	github.com/anacrolix/torrent v1.53.2
 	github.com/billziss-gh/cgofuse v1.5.0
-	github.com/bodgit/sevenzip v1.4.3
+	github.com/bodgit/sevenzip v1.4.5
 	github.com/dgraph-io/badger/v4 v4.2.0
+	github.com/edsrzf/mmap-go v1.1.0
+	github.com/gin-contrib/pprof v1.4.0
 	github.com/gin-gonic/gin v1.9.1
+	github.com/go-git/go-billy/v5 v5.5.0
 	github.com/knadh/koanf/parsers/yaml v0.1.0
 	github.com/knadh/koanf/providers/env v0.1.0
 	github.com/knadh/koanf/providers/file v0.1.0
@@ -21,122 +25,124 @@ require (
 	github.com/rs/zerolog v1.31.0
 	github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c
 	github.com/stretchr/testify v1.8.4
-	github.com/urfave/cli/v2 v2.25.7
-	golang.org/x/net v0.16.0
+	github.com/urfave/cli/v2 v2.26.0
+	github.com/willscott/go-nfs v0.0.1
+	golang.org/x/net v0.19.0
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1
 )
 
 require (
-	github.com/RoaringBitmap/roaring v1.2.3 // indirect
+	github.com/RoaringBitmap/roaring v1.6.0 // indirect
 	github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect
 	github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
 	github.com/anacrolix/chansync v0.3.0 // indirect
 	github.com/anacrolix/envpprof v1.3.0 // indirect
-	github.com/anacrolix/generics v0.0.0-20230816105729-c755655aee45 // indirect
+	github.com/anacrolix/generics v0.0.0-20230911070922-5dd7545c6b13 // indirect
 	github.com/anacrolix/go-libutp v1.3.1 // indirect
-	github.com/anacrolix/missinggo v1.3.0 // indirect
 	github.com/anacrolix/missinggo/perf v1.0.0 // indirect
 	github.com/anacrolix/mmsg v1.0.0 // indirect
-	github.com/anacrolix/multiless v0.3.1-0.20221221005021-2d12701f83f7 // indirect
-	github.com/anacrolix/stm v0.4.1-0.20221221005312-96d17df0e496 // indirect
-	github.com/anacrolix/sync v0.4.1-0.20230926072150-b8cd7cfb92d0 // indirect
-	github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96 // indirect
-	github.com/anacrolix/utp v0.1.0 // indirect
-	github.com/andybalholm/brotli v1.0.5 // indirect
+	github.com/anacrolix/multiless v0.3.1-0.20230203023154-f3d27407d8f1 // indirect
+	github.com/anacrolix/stm v0.5.0 // indirect
+	github.com/anacrolix/sync v0.5.1 // indirect
+	github.com/anacrolix/upnp v0.1.3 // indirect
+	github.com/anacrolix/utp v0.2.0 // indirect
+	github.com/andybalholm/brotli v1.0.6 // indirect
 	github.com/bahlo/generic-list-go v0.2.0 // indirect
-	github.com/benbjohnson/immutable v0.4.1-0.20221220213129-8932b999621d // indirect
-	github.com/bits-and-blooms/bitset v1.2.2 // indirect
+	github.com/benbjohnson/immutable v0.4.3 // indirect
+	github.com/bits-and-blooms/bitset v1.12.0 // indirect
 	github.com/bodgit/plumbing v1.3.0 // indirect
 	github.com/bodgit/windows v1.0.1 // indirect
 	github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
-	github.com/bytedance/sonic v1.9.1 // indirect
+	github.com/bytedance/sonic v1.10.2 // indirect
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
-	github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
-	github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
+	github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
+	github.com/chenzhuoyu/iasm v0.9.1 // indirect
+	github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/dgraph-io/ristretto v0.1.1 // indirect
-	github.com/dustin/go-humanize v1.0.0 // indirect
-	github.com/edsrzf/mmap-go v1.1.0 // indirect
+	github.com/dustin/go-humanize v1.0.1 // indirect
 	github.com/fatih/structs v1.1.0 // indirect
-	github.com/fsnotify/fsnotify v1.6.0 // indirect
-	github.com/gabriel-vasile/mimetype v1.4.2 // indirect
+	github.com/fsnotify/fsnotify v1.7.0 // indirect
+	github.com/gabriel-vasile/mimetype v1.4.3 // indirect
 	github.com/gin-contrib/sse v0.1.0 // indirect
 	github.com/go-llsqlite/adapter v0.0.0-20230927005056-7f5ce7f0c916 // indirect
 	github.com/go-llsqlite/crawshaw v0.4.0 // indirect
-	github.com/go-logr/logr v1.2.3 // indirect
+	github.com/go-logr/logr v1.3.0 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-playground/locales v0.14.1 // indirect
 	github.com/go-playground/universal-translator v0.18.1 // indirect
-	github.com/go-playground/validator/v10 v10.14.0 // indirect
+	github.com/go-playground/validator/v10 v10.16.0 // indirect
 	github.com/goccy/go-json v0.10.2 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
-	github.com/golang/glog v1.0.0 // indirect
-	github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
-	github.com/golang/protobuf v1.5.2 // indirect
-	github.com/golang/snappy v0.0.3 // indirect
+	github.com/golang/glog v1.2.0 // indirect
+	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
+	github.com/golang/protobuf v1.5.3 // indirect
+	github.com/golang/snappy v0.0.4 // indirect
 	github.com/google/btree v1.1.2 // indirect
-	github.com/google/flatbuffers v1.12.1 // indirect
-	github.com/google/uuid v1.3.0 // indirect
-	github.com/gorilla/websocket v1.5.0 // indirect
-	github.com/hashicorp/errwrap v1.0.0 // indirect
+	github.com/google/flatbuffers v23.5.26+incompatible // indirect
+	github.com/google/uuid v1.5.0 // indirect
+	github.com/gorilla/websocket v1.5.1 // indirect
+	github.com/hashicorp/errwrap v1.1.0 // indirect
 	github.com/hashicorp/go-multierror v1.1.1 // indirect
-	github.com/huandu/xstrings v1.3.2 // indirect
+	github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
+	github.com/huandu/xstrings v1.4.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
-	github.com/klauspost/compress v1.16.6 // indirect
-	github.com/klauspost/cpuid/v2 v2.2.4 // indirect
+	github.com/klauspost/compress v1.17.4 // indirect
+	github.com/klauspost/cpuid/v2 v2.2.6 // indirect
 	github.com/knadh/koanf/maps v0.1.1 // indirect
 	github.com/leodido/go-urn v1.2.4 // indirect
-	github.com/mattn/go-isatty v0.0.19 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/mitchellh/copystructure v1.2.0 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/mitchellh/reflectwalk v1.0.2 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/mschoch/smat v0.2.0 // indirect
-	github.com/pelletier/go-toml/v2 v2.0.8 // indirect
-	github.com/pierrec/lz4/v4 v4.1.18 // indirect
-	github.com/pion/datachannel v1.5.2 // indirect
-	github.com/pion/dtls/v2 v2.2.4 // indirect
-	github.com/pion/ice/v2 v2.2.6 // indirect
-	github.com/pion/interceptor v0.1.11 // indirect
+	github.com/pelletier/go-toml/v2 v2.1.1 // indirect
+	github.com/pierrec/lz4/v4 v4.1.19 // indirect
+	github.com/pion/datachannel v1.5.5 // indirect
+	github.com/pion/dtls/v2 v2.2.8 // indirect
+	github.com/pion/ice/v2 v2.3.11 // indirect
+	github.com/pion/interceptor v0.1.25 // indirect
 	github.com/pion/logging v0.2.2 // indirect
-	github.com/pion/mdns v0.0.5 // indirect
+	github.com/pion/mdns v0.0.9 // indirect
 	github.com/pion/randutil v0.1.0 // indirect
-	github.com/pion/rtcp v1.2.9 // indirect
-	github.com/pion/rtp v1.7.13 // indirect
-	github.com/pion/sctp v1.8.2 // indirect
-	github.com/pion/sdp/v3 v3.0.5 // indirect
-	github.com/pion/srtp/v2 v2.0.9 // indirect
-	github.com/pion/stun v0.3.5 // indirect
-	github.com/pion/transport v0.13.1 // indirect
-	github.com/pion/transport/v2 v2.0.0 // indirect
-	github.com/pion/turn/v2 v2.0.8 // indirect
-	github.com/pion/udp v0.1.4 // indirect
-	github.com/pion/webrtc/v3 v3.1.42 // indirect
+	github.com/pion/rtcp v1.2.13 // indirect
+	github.com/pion/rtp v1.8.3 // indirect
+	github.com/pion/sctp v1.8.9 // indirect
+	github.com/pion/sdp/v3 v3.0.6 // indirect
+	github.com/pion/srtp/v2 v2.0.18 // indirect
+	github.com/pion/stun v0.6.1 // indirect
+	github.com/pion/transport/v2 v2.2.4 // indirect
+	github.com/pion/turn/v2 v2.1.4 // indirect
+	github.com/pion/webrtc/v3 v3.2.24 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93 // indirect
 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
-	github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect
+	github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect
-	github.com/tidwall/btree v1.6.0 // indirect
+	github.com/tidwall/btree v1.7.0 // indirect
 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
-	github.com/ugorji/go/codec v1.2.11 // indirect
+	github.com/ugorji/go/codec v1.2.12 // indirect
 	github.com/ulikunitz/xz v0.5.11 // indirect
-	github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
-	go.etcd.io/bbolt v1.3.6 // indirect
-	go.opencensus.io v0.22.5 // indirect
-	go.opentelemetry.io/otel v1.8.0 // indirect
-	go.opentelemetry.io/otel/trace v1.8.0 // indirect
-	go4.org v0.0.0-20200411211856-f5505b9728dd // indirect
-	golang.org/x/arch v0.3.0 // indirect
-	golang.org/x/crypto v0.14.0 // indirect
-	golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect
-	golang.org/x/sync v0.3.0 // indirect
-	golang.org/x/sys v0.13.0 // indirect
-	golang.org/x/text v0.13.0 // indirect
-	golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
-	google.golang.org/protobuf v1.30.0 // indirect
+	github.com/willscott/go-nfs-client v0.0.0-20200605172546-271fa9065b33 // indirect
+	github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect
+	go.etcd.io/bbolt v1.3.8 // indirect
+	go.opencensus.io v0.24.0 // indirect
+	go.opentelemetry.io/otel v1.21.0 // indirect
+	go.opentelemetry.io/otel/metric v1.21.0 // indirect
+	go.opentelemetry.io/otel/trace v1.21.0 // indirect
+	go4.org v0.0.0-20230225012048-214862532bf5 // indirect
+	golang.org/x/arch v0.6.0 // indirect
+	golang.org/x/crypto v0.16.0 // indirect
+	golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 // indirect
+	golang.org/x/sync v0.5.0 // indirect
+	golang.org/x/sys v0.15.0 // indirect
+	golang.org/x/text v0.14.0 // indirect
+	golang.org/x/time v0.5.0 // indirect
+	google.golang.org/protobuf v1.31.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	modernc.org/libc v1.22.3 // indirect
 	modernc.org/mathutil v1.5.0 // indirect
diff --git a/go.sum b/go.sum
index 14db9e5..b8ad50d 100644
--- a/go.sum
+++ b/go.sum
@@ -24,8 +24,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
 github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w=
 github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI=
 github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
-github.com/RoaringBitmap/roaring v1.2.3 h1:yqreLINqIrX22ErkKI0vY47/ivtJr6n+kMhVOVmhWBY=
-github.com/RoaringBitmap/roaring v1.2.3/go.mod h1:plvDsJQpxOC5bw8LRteu/MLWHsHez/3y6cubLI4/1yE=
+github.com/RoaringBitmap/roaring v1.6.0 h1:dc7kRiroETgJcHhWX6BerXkZz2b3JgLGg9nTURJL/og=
+github.com/RoaringBitmap/roaring v1.6.0/go.mod h1:plvDsJQpxOC5bw8LRteu/MLWHsHez/3y6cubLI4/1yE=
 github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
 github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
 github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 h1:byYvvbfSo3+9efR4IeReh77gVs4PnNDR3AMOE9NJ7a0=
@@ -42,23 +42,24 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
 github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/anacrolix/chansync v0.3.0 h1:lRu9tbeuw3wl+PhMu/r+JJCRu5ArFXIluOgdF0ao6/U=
 github.com/anacrolix/chansync v0.3.0/go.mod h1:DZsatdsdXxD0WiwcGl0nJVwyjCKMDv+knl1q2iBjA2k=
-github.com/anacrolix/dht/v2 v2.20.0 h1:eDx9lfE9iCSf5sPK0290GToHURNhEFuUGN8iyvhvJDk=
-github.com/anacrolix/dht/v2 v2.20.0/go.mod h1:SDGC+sEs1pnO2sJGYuhvIis7T8749dDHNfcjtdH4e3g=
+github.com/anacrolix/dht/v2 v2.21.0 h1:8nzI+faaynY9jOKmVgdmBZVrTo8B7ZE/LKEgN3Vl/Bs=
+github.com/anacrolix/dht/v2 v2.21.0/go.mod h1:SDGC+sEs1pnO2sJGYuhvIis7T8749dDHNfcjtdH4e3g=
 github.com/anacrolix/envpprof v0.0.0-20180404065416-323002cec2fa/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
 github.com/anacrolix/envpprof v1.0.0/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
 github.com/anacrolix/envpprof v1.1.0/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4=
 github.com/anacrolix/envpprof v1.3.0 h1:WJt9bpuT7A/CDCxPOv/eeZqHWlle/Y0keJUvc6tcJDk=
 github.com/anacrolix/envpprof v1.3.0/go.mod h1:7QIG4CaX1uexQ3tqd5+BRa/9e2D02Wcertl6Yh0jCB0=
-github.com/anacrolix/generics v0.0.0-20230816105729-c755655aee45 h1:Kmcl3I9K2+5AdnnR7hvrnVT0TLeFWWMa9bxnm55aVIg=
-github.com/anacrolix/generics v0.0.0-20230816105729-c755655aee45/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8=
+github.com/anacrolix/generics v0.0.0-20230113004304-d6428d516633/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8=
+github.com/anacrolix/generics v0.0.0-20230911070922-5dd7545c6b13 h1:qwOprPTDMM3BASJRf84mmZnTXRsPGGJ8xoHKQS7m3so=
+github.com/anacrolix/generics v0.0.0-20230911070922-5dd7545c6b13/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8=
 github.com/anacrolix/go-libutp v1.3.1 h1:idJzreNLl+hNjGC3ZnUOjujEaryeOGgkwHLqSGoige0=
 github.com/anacrolix/go-libutp v1.3.1/go.mod h1:heF41EC8kN0qCLMokLBVkB8NXiLwx3t8R8810MTNI5o=
 github.com/anacrolix/log v0.3.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU=
 github.com/anacrolix/log v0.6.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU=
-github.com/anacrolix/log v0.10.1-0.20220123034749-3920702c17f8/go.mod h1:GmnE2c0nvz8pOIPUSC9Rawgefy1sDXqposC2wgtBZE4=
 github.com/anacrolix/log v0.13.1/go.mod h1:D4+CvN8SnruK6zIFS/xPoRJmtvtnxs+CSfDQ+BFxZ68=
-github.com/anacrolix/log v0.14.3-0.20230823030427-4b296d71a6b4 h1:01OE3pdiBGIZGyQb6cIAu+QfaNhBR9k5MVmLsl+DVbE=
-github.com/anacrolix/log v0.14.3-0.20230823030427-4b296d71a6b4/go.mod h1:1OmJESOtxQGNMlUO5rcv96Vpp9mfMqXXbe2RdinFLdY=
+github.com/anacrolix/log v0.14.2/go.mod h1:1OmJESOtxQGNMlUO5rcv96Vpp9mfMqXXbe2RdinFLdY=
+github.com/anacrolix/log v0.14.5 h1:OkMjBquVSRb742LkecSGDGaGpNoSrw4syRIm0eRdmrg=
+github.com/anacrolix/log v0.14.5/go.mod h1:1OmJESOtxQGNMlUO5rcv96Vpp9mfMqXXbe2RdinFLdY=
 github.com/anacrolix/lsan v0.0.0-20211126052245-807000409a62 h1:P04VG6Td13FHMgS5ZBcJX23NPC/fiC4cp9bXwYujdYM=
 github.com/anacrolix/lsan v0.0.0-20211126052245-807000409a62/go.mod h1:66cFKPCO7Sl4vbFnAaSq7e4OXtdMhRSBagJGWgmpJbM=
 github.com/anacrolix/missinggo v0.0.0-20180725070939-60ef2fbf63df/go.mod h1:kwGiTUTZ0+p4vAz3VbAI5a30t2YbvemcmspjKwrAz5s=
@@ -71,50 +72,49 @@ github.com/anacrolix/missinggo/perf v1.0.0 h1:7ZOGYziGEBytW49+KmYGTaNfnwUqP1HBsy
 github.com/anacrolix/missinggo/perf v1.0.0/go.mod h1:ljAFWkBuzkO12MQclXzZrosP5urunoLS0Cbvb4V0uMQ=
 github.com/anacrolix/missinggo/v2 v2.2.0/go.mod h1:o0jgJoYOyaoYQ4E2ZMISVa9c88BbUBVQQW4QeRkNCGY=
 github.com/anacrolix/missinggo/v2 v2.5.1/go.mod h1:WEjqh2rmKECd0t1VhQkLGTdIWXO6f6NLjp5GlMZ+6FA=
-github.com/anacrolix/missinggo/v2 v2.5.2/go.mod h1:yNvsLrtZYRYCOI+KRH/JM8TodHjtIE/bjOGhQaLOWIE=
-github.com/anacrolix/missinggo/v2 v2.7.2 h1:XGia0kZVC8DDY6XVl15fjtdEyUF39tWkdtsH1VjuAHg=
-github.com/anacrolix/missinggo/v2 v2.7.2/go.mod h1:mIEtp9pgaXqt8VQ3NQxFOod/eQ1H0D1XsZzKUQfwtac=
+github.com/anacrolix/missinggo/v2 v2.7.3 h1:Ee//CmZBMadeNiYB/hHo9ly2PFOEZ4Fhsbnug3rDAIE=
+github.com/anacrolix/missinggo/v2 v2.7.3/go.mod h1:mIEtp9pgaXqt8VQ3NQxFOod/eQ1H0D1XsZzKUQfwtac=
 github.com/anacrolix/mmsg v0.0.0-20180515031531-a4a3ba1fc8bb/go.mod h1:x2/ErsYUmT77kezS63+wzZp8E3byYB0gzirM/WMBLfw=
 github.com/anacrolix/mmsg v1.0.0 h1:btC7YLjOn29aTUAExJiVUhQOuf/8rhm+/nWCMAnL3Hg=
 github.com/anacrolix/mmsg v1.0.0/go.mod h1:x8kRaJY/dCrY9Al0PEcj1mb/uFHwP6GCJ9fLl4thEPc=
-github.com/anacrolix/multiless v0.3.1-0.20221221005021-2d12701f83f7 h1:lOtCD+LzoD1g7bowhYJNR++uV+FyY5bTZXKwnPex9S8=
-github.com/anacrolix/multiless v0.3.1-0.20221221005021-2d12701f83f7/go.mod h1:zJv1JF9AqdZiHwxqPgjuOZDGWER6nyE48WBCi/OOrMM=
+github.com/anacrolix/multiless v0.3.1-0.20230203023154-f3d27407d8f1 h1:1gfWAUiwUurVDZ4Re9e1hhpF0iGLlVBhPL5DY5U5hrI=
+github.com/anacrolix/multiless v0.3.1-0.20230203023154-f3d27407d8f1/go.mod h1:zJv1JF9AqdZiHwxqPgjuOZDGWER6nyE48WBCi/OOrMM=
 github.com/anacrolix/stm v0.2.0/go.mod h1:zoVQRvSiGjGoTmbM0vSLIiaKjWtNPeTvXUSdJQA4hsg=
-github.com/anacrolix/stm v0.4.1-0.20221221005312-96d17df0e496 h1:aMiRi2kOOd+nG64suAmFMVnNK2E6GsnLif7ia9tI3cA=
-github.com/anacrolix/stm v0.4.1-0.20221221005312-96d17df0e496/go.mod h1:DBm8/1OXm4A4RZ6Xa9u/eOsjeAXCaoRYvd2JzlskXeM=
+github.com/anacrolix/stm v0.5.0 h1:9df1KBpttF0TzLgDq51Z+TEabZKMythqgx89f1FQJt8=
+github.com/anacrolix/stm v0.5.0/go.mod h1:MOwrSy+jCm8Y7HYfMAwPj7qWVu7XoVvjOiYwJmpeB/M=
 github.com/anacrolix/sync v0.0.0-20180808010631-44578de4e778/go.mod h1:s735Etp3joe/voe2sdaXLcqDdJSay1O0OPnM0ystjqk=
 github.com/anacrolix/sync v0.3.0/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DCOj/I0g=
-github.com/anacrolix/sync v0.4.1-0.20230926072150-b8cd7cfb92d0 h1:M2HtYrYz6CVwo88TfVrGNlc+mSe59KXCBe3gFuEsEto=
-github.com/anacrolix/sync v0.4.1-0.20230926072150-b8cd7cfb92d0/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DCOj/I0g=
+github.com/anacrolix/sync v0.5.1 h1:FbGju6GqSjzVoTgcXTUKkF041lnZkG5P0C3T5RL3SGc=
+github.com/anacrolix/sync v0.5.1/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DCOj/I0g=
 github.com/anacrolix/tagflag v0.0.0-20180109131632-2146c8d41bf0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw=
 github.com/anacrolix/tagflag v1.0.0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw=
 github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8=
-github.com/anacrolix/torrent v1.52.6-0.20230929044811-45c91b322ad1 h1:KzIKTajeqBXWeLjHv2KHjlwigyR19TkdvU5uLGPGQAI=
-github.com/anacrolix/torrent v1.52.6-0.20230929044811-45c91b322ad1/go.mod h1:q4utKicrzW80odcXiy3J8sObJELsGGFI1FxhFt/2qA0=
-github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96 h1:QAVZ3pN/J4/UziniAhJR2OZ9Ox5kOY2053tBbbqUPYA=
-github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96/go.mod h1:Wa6n8cYIdaG35x15aH3Zy6d03f7P728QfdcDeD/IEOs=
-github.com/anacrolix/utp v0.1.0 h1:FOpQOmIwYsnENnz7tAGohA+r6iXpRjrq8ssKSre2Cp4=
-github.com/anacrolix/utp v0.1.0/go.mod h1:MDwc+vsGEq7RMw6lr2GKOEqjWny5hO5OZXRVNaBJ2Dk=
-github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
-github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
+github.com/anacrolix/torrent v1.53.2 h1:dW+ficSC8sJaGrUvZJizORPBLTP7XR8idl2oGlrUutQ=
+github.com/anacrolix/torrent v1.53.2/go.mod h1:d1NANCFAd9/nv9vmHnYUobLdyBSAoFYohojHjGmcAsw=
+github.com/anacrolix/upnp v0.1.3 h1:NlYEhE75adz2npEJKjbqyqnyW9qU4STookvSNXBJ5ao=
+github.com/anacrolix/upnp v0.1.3/go.mod h1:Qyhbqo69gwNWvEk1xNTXsS5j7hMHef9hdr984+9fIic=
+github.com/anacrolix/utp v0.2.0 h1:65Cdmr6q9WSw2KsM+rtJFu7rqDzLl2bdysf4KlNPcFI=
+github.com/anacrolix/utp v0.2.0/go.mod h1:HGk4GYQw1O/3T1+yhqT/F6EcBd+AAwlo9dYErNy7mj8=
+github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
+github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
 github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
 github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
 github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
 github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI=
-github.com/benbjohnson/immutable v0.4.1-0.20221220213129-8932b999621d h1:2qVb9bsAMtmAfnxXltm+6eBzrrS7SZ52c3SedsulaMI=
-github.com/benbjohnson/immutable v0.4.1-0.20221220213129-8932b999621d/go.mod h1:iAr8OjJGLnLmVUr9MZ/rz4PWUy6Ouc2JLYuMArmvAJM=
+github.com/benbjohnson/immutable v0.4.3 h1:GYHcksoJ9K6HyAUpGxwZURrbTkXA0Dh4otXGqbhdrjA=
+github.com/benbjohnson/immutable v0.4.3/go.mod h1:qJIKKSmdqz1tVzNtst1DZzvaqOU1onk1rc03IeM3Owk=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/billziss-gh/cgofuse v1.5.0 h1:kH516I/s+Ab4diL/Y/ayFeUjjA8ey+JK12xDfBf4HEs=
 github.com/billziss-gh/cgofuse v1.5.0/go.mod h1:LJjoaUojlVjgo5GQoEJTcJNqZJeRU0nCR84CyxKt2YM=
 github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
-github.com/bits-and-blooms/bitset v1.2.2 h1:J5gbX05GpMdBjCvQ9MteIg2KKDExr7DrgK+Yc15FvIk=
-github.com/bits-and-blooms/bitset v1.2.2/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
+github.com/bits-and-blooms/bitset v1.12.0 h1:U/q1fAF7xXRhFCrhROzIfffYnu+dlS38vCZtmFVPHmA=
+github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
 github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
 github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
-github.com/bodgit/sevenzip v1.4.3 h1:46Rb9vCYdpceC1U+GIR0bS3hP2/Xv8coKFDeLJySV/A=
-github.com/bodgit/sevenzip v1.4.3/go.mod h1:F8n3+0CwbdxqmNy3wFeOAtanza02Ur66AGfs/hbYblI=
+github.com/bodgit/sevenzip v1.4.5 h1:HFJQ+nbjppfyf2xbQEJBbmVo+o2kTg1FXV4i7YOx87s=
+github.com/bodgit/sevenzip v1.4.5/go.mod h1:LAcAg/UQzyjzCQSGBPZFYzoiHMfT6Gk+3tMSjUk3foY=
 github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
 github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
 github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo=
@@ -122,22 +122,28 @@ github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2w
 github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8=
 github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8/go.mod h1:spo1JLcs67NmW1aVLEgtA8Yy1elc+X8y5SRW1sFW4Og=
 github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
-github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
-github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
+github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
+github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=
+github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
 github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
-github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
 github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
+github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
+github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
+github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
+github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
+github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
-github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
-github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
+github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -150,28 +156,35 @@ github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczC
 github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
 github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
 github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
-github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
 github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
 github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
 github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ=
 github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
 github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
 github.com/frankban/quicktest v1.9.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
+github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
 github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
 github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
-github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
-github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
-github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
-github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
+github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
+github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
+github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
+github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
+github.com/gin-contrib/pprof v1.4.0 h1:XxiBSf5jWZ5i16lNOPbMTVdgHBdhfGRD5PZ1LWazzvg=
+github.com/gin-contrib/pprof v1.4.0/go.mod h1:RrehPJasUVBPK6yTUwOl8/NP6i0vbUgmxtis+Z5KE90=
 github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
 github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
 github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
 github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
 github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
@@ -180,6 +193,8 @@ github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a/go.mod
 github.com/glycerine/goconvey v0.0.0-20180728074245-46e3a41ad493/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
 github.com/glycerine/goconvey v0.0.0-20190315024820-982ee783a72e/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
 github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
+github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
+github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@@ -191,20 +206,25 @@ github.com/go-llsqlite/crawshaw v0.4.0/go.mod h1:/YJdV7uBQaYDE0fwe4z3wwJIZBJxdYz
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
-github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
+github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
 github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
 github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
 github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
 github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
 github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
-github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
-github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
+github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
+github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE=
+github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
+github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
 github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@@ -213,12 +233,13 @@ github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
-github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
+github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68=
+github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
 github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
@@ -232,36 +253,44 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x
 github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
 github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
 github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
 github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
-github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
+github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
 github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
-github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw=
-github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
+github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg=
+github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
-github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
-github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
+github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@@ -270,14 +299,17 @@ github.com/gopherjs/gopherjs v0.0.0-20190309154008-847fc94819f9/go.mod h1:wJfORR
 github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
 github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
-github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
-github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
+github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
+github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
+github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
 github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
+github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
 github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
 github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
@@ -285,8 +317,8 @@ github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbc
 github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
 github.com/huandu/xstrings v1.3.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
 github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
-github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
-github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
+github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
+github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@@ -299,11 +331,11 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/klauspost/compress v1.16.6 h1:91SKEy4K37vkp255cJ8QesJhjyRO0hn9i9G0GoUwLsk=
-github.com/klauspost/compress v1.16.6/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
+github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
+github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
 github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
-github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
-github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
+github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
+github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
 github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs=
 github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
 github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w=
@@ -316,10 +348,12 @@ github.com/knadh/koanf/providers/structs v0.1.0 h1:wJRteCNn1qvLtE5h8KQBvLJovidSd
 github.com/knadh/koanf/providers/structs v0.1.0/go.mod h1:sw2YZ3txUcqA3Z27gPlmmBzWn1h8Nt9O6EP/91MkcWE=
 github.com/knadh/koanf/v2 v2.0.1 h1:1dYGITt1I23x8cfx8ZnldtezdyaZtfAuRtIFOiRzK7g=
 github.com/knadh/koanf/v2 v2.0.1/go.mod h1:ZeiIlIDXTE7w1lMT6UVcNiRAS2/rCeLn/GdLNvY1Dus=
+github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -327,13 +361,16 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
 github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
 github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
 github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
 github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
 github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
@@ -366,55 +403,60 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J
 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
 github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
 github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
-github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
-github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
+github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
+github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
+github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
 github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
-github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
-github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
-github.com/pion/datachannel v1.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E=
-github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ=
-github.com/pion/dtls/v2 v2.1.3/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
-github.com/pion/dtls/v2 v2.1.5/go.mod h1:BqCE7xPZbPSubGasRoDFJeTsyJtdD1FanJYL0JGheqY=
-github.com/pion/dtls/v2 v2.2.4 h1:YSfYwDQgrxMYXLBc/m7PFY5BVtWlNm/DN4qoU2CbcWg=
-github.com/pion/dtls/v2 v2.2.4/go.mod h1:WGKfxqhrddne4Kg3p11FUMJrynkOY4lb25zHNO49wuw=
-github.com/pion/ice/v2 v2.2.6 h1:R/vaLlI1J2gCx141L5PEwtuGAGcyS6e7E0hDeJFq5Ig=
-github.com/pion/ice/v2 v2.2.6/go.mod h1:SWuHiOGP17lGromHTFadUe1EuPgFh/oCU6FCMZHooVE=
-github.com/pion/interceptor v0.1.11 h1:00U6OlqxA3FFB50HSg25J/8cWi7P6FbSzw4eFn24Bvs=
-github.com/pion/interceptor v0.1.11/go.mod h1:tbtKjZY14awXd7Bq0mmWvgtHB5MDaRN7HV3OZ/uy7s8=
+github.com/pierrec/lz4/v4 v4.1.19 h1:tYLzDnjDXh9qIxSTKHwXwOYmm9d887Y7Y1ZkyXYHAN4=
+github.com/pierrec/lz4/v4 v4.1.19/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
+github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8=
+github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
+github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
+github.com/pion/dtls/v2 v2.2.8 h1:BUroldfiIbV9jSnC6cKOMnyiORRWrWWpV11JUyEu5OA=
+github.com/pion/dtls/v2 v2.2.8/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
+github.com/pion/ice/v2 v2.3.11 h1:rZjVmUwyT55cmN8ySMpL7rsS8KYsJERsrxJLLxpKhdw=
+github.com/pion/ice/v2 v2.3.11/go.mod h1:hPcLC3kxMa+JGRzMHqQzjoSj3xtE9F+eoncmXLlCL4E=
+github.com/pion/interceptor v0.1.25 h1:pwY9r7P6ToQ3+IF0bajN0xmk/fNw/suTgaTdlwTDmhc=
+github.com/pion/interceptor v0.1.25/go.mod h1:wkbPYAak5zKsfpVDYMtEfWEy8D4zL+rpxCxPImLOg3Y=
 github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
 github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
-github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw=
-github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g=
+github.com/pion/mdns v0.0.8/go.mod h1:hYE72WX8WDveIhg7fmXgMKivD3Puklk0Ymzog0lSyaI=
+github.com/pion/mdns v0.0.9 h1:7Ue5KZsqq8EuqStnpPWV33vYYEH0+skdDN5L7EiEsI4=
+github.com/pion/mdns v0.0.9/go.mod h1:2JA5exfxwzXiCihmxpTKgFUpiQws2MnipoPK09vecIc=
 github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
 github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
-github.com/pion/rtcp v1.2.9 h1:1ujStwg++IOLIEoOiIQ2s+qBuJ1VN81KW+9pMPsif+U=
-github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo=
-github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA=
-github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
-github.com/pion/sctp v1.8.0/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
-github.com/pion/sctp v1.8.2 h1:yBBCIrUMJ4yFICL3RIvR4eh/H2BTTvlligmSTy+3kiA=
-github.com/pion/sctp v1.8.2/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
-github.com/pion/sdp/v3 v3.0.5 h1:ouvI7IgGl+V4CrqskVtr3AaTrPvPisEOxwgpdktctkU=
-github.com/pion/sdp/v3 v3.0.5/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
-github.com/pion/srtp/v2 v2.0.9 h1:JJq3jClmDFBPX/F5roEb0U19jSU7eUhyDqR/NZ34EKQ=
-github.com/pion/srtp/v2 v2.0.9/go.mod h1:5TtM9yw6lsH0ppNCehB/EjEUli7VkUgKSPJqWVqbhQ4=
-github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
-github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
-github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
-github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A=
-github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g=
-github.com/pion/transport v0.13.1 h1:/UH5yLeQtwm2VZIPjxwnNFxjS4DFhyLfS4GlfuKUzfA=
-github.com/pion/transport v0.13.1/go.mod h1:EBxbqzyv+ZrmDb82XswEE0BjfQFtuw1Nu6sjnjWCsGg=
-github.com/pion/transport/v2 v2.0.0 h1:bsMYyqHCbkvHwj+eNCFBuxtlKndKfyGI2vaQmM3fIE4=
-github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=
-github.com/pion/turn/v2 v2.0.8 h1:KEstL92OUN3k5k8qxsXHpr7WWfrdp7iJZHx99ud8muw=
-github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
-github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
-github.com/pion/udp v0.1.4 h1:OowsTmu1Od3sD6i3fQUJxJn2fEvJO6L1TidgadtbTI8=
-github.com/pion/udp v0.1.4/go.mod h1:G8LDo56HsFwC24LIcnT4YIDU5qcB6NepqqjP0keL2us=
-github.com/pion/webrtc/v3 v3.1.42 h1:wJEQFIXVanptnQcHOLTuIo4AtGB2+mG2x4OhIhnITOA=
-github.com/pion/webrtc/v3 v3.1.42/go.mod h1:ffD9DulDrPxyWvDPUIPAOSAWx9GUlOExiJPf7cCcMLA=
+github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
+github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
+github.com/pion/rtcp v1.2.13 h1:+EQijuisKwm/8VBs8nWllr0bIndR7Lf7cZG200mpbNo=
+github.com/pion/rtcp v1.2.13/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
+github.com/pion/rtp v1.8.2/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
+github.com/pion/rtp v1.8.3 h1:VEHxqzSVQxCkKDSHro5/4IUUG1ea+MFdqR2R3xSpNU8=
+github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
+github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
+github.com/pion/sctp v1.8.8/go.mod h1:igF9nZBrjh5AtmKc7U30jXltsFHicFCXSmWA2GWRaWs=
+github.com/pion/sctp v1.8.9 h1:TP5ZVxV5J7rz7uZmbyvnUvsn7EJ2x/5q9uhsTtXbI3g=
+github.com/pion/sctp v1.8.9/go.mod h1:cMLT45jqw3+jiJCrtHVwfQLnfR0MGZ4rgOJwUOIqLkI=
+github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw=
+github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
+github.com/pion/srtp/v2 v2.0.18 h1:vKpAXfawO9RtTRKZJbG4y0v1b11NZxQnxRl85kGuUlo=
+github.com/pion/srtp/v2 v2.0.18/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA=
+github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
+github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
+github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
+github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
+github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
+github.com/pion/transport/v2 v2.2.2/go.mod h1:OJg3ojoBJopjEeECq2yJdXH9YVrUJ1uQ++NjXLOUorc=
+github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
+github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo=
+github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
+github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM=
+github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
+github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
+github.com/pion/turn/v2 v2.1.4 h1:2xn8rduI5W6sCZQkEnIUDAkrBQNl2eYIBCHMZ3QMmP8=
+github.com/pion/turn/v2 v2.1.4/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
+github.com/pion/webrtc/v3 v3.2.24 h1:MiFL5DMo2bDaaIFWr0DDpwiV/L4EGbLZb+xoRvfEo1Y=
+github.com/pion/webrtc/v3 v3.2.24/go.mod h1:1CaT2fcZzZ6VZA+O1i9yK2DU4EOcXVvSbWG9pr5jefs=
 github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -439,6 +481,8 @@ github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R
 github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
 github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
+github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93 h1:UVArwN/wkKjMVhh2EQGC0tEc1+FqiLlvYXY5mQ2f8Wg=
+github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93/go.mod h1:Nfe4efndBz4TibWycNE+lqyJZiMX4ycx+QKV8Ta0f/o=
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
@@ -446,10 +490,11 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
 github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
-github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
 github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
-github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 h1:Lt9DzQALzHoDwMBGJ6v8ObDPR0dzr2a6sXTB1Fq7IHs=
-github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA=
+github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
+github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
+github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 h1:18kd+8ZUlt/ARXhljq+14TwAoKa61q6dX8jtwOf6DH8=
+github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA=
 github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
 github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
 github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
@@ -485,58 +530,70 @@ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
 github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/tidwall/btree v1.6.0 h1:LDZfKfQIBHGHWSwckhXI0RPSXzlo+KYdjK7FWSqOzzg=
-github.com/tidwall/btree v1.6.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
+github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI=
+github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
 github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
 github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
 github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
 github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
 github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
-github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
-github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
+github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
+github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
+github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
 github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
 github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
-github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
-github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
+github.com/urfave/cli/v2 v2.26.0 h1:3f3AMg3HpThFNT4I++TKOejZO8yU55t3JnnSr4S4QEI=
+github.com/urfave/cli/v2 v2.26.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
 github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
 github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
-github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
-github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
+github.com/willscott/go-nfs v0.0.1 h1:392gV283iuisKFeV9hkKwTdCRfizP+R9FC+gYg2skj0=
+github.com/willscott/go-nfs v0.0.1/go.mod h1:hBPyqKNde3v8rzxDVWtloP6MtLnx/7aVz3XxxP89W7k=
+github.com/willscott/go-nfs-client v0.0.0-20200605172546-271fa9065b33 h1:Wd8wdpRzPXskyHvZLyw7Wc1fp5oCE2mhBCj7bAiibUs=
+github.com/willscott/go-nfs-client v0.0.0-20200605172546-271fa9065b33/go.mod h1:cOUKSNty+RabZqKhm5yTJT5Vq/Fe83ZRWAJ5Kj8nRes=
+github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI=
+github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
-go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
+github.com/zema1/go-nfs-client v0.0.0-20200604081958-0cf942f0e0fe/go.mod h1:im3CVJ32XM3+E+2RhY0sa5IVJVQehUrX0oE1wX4xOwU=
+go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
+go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
 go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
 go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
 go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
 go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
 go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0=
-go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
-go.opentelemetry.io/otel v1.8.0 h1:zcvBFizPbpa1q7FehvFiHbQwGzmPILebO0tyqIR5Djg=
-go.opentelemetry.io/otel v1.8.0/go.mod h1:2pkj+iMj0o03Y+cW6/m8Y4WkRdYN3AvCXCnzRMp9yvM=
-go.opentelemetry.io/otel/trace v1.8.0 h1:cSy0DF9eGI5WIfNwZ1q2iUyGj00tGzP24dE1lOlHrfY=
-go.opentelemetry.io/otel/trace v1.8.0/go.mod h1:0Bt3PXY8w+3pheS3hQUt+wow8b1ojPaTBoTCh2zIFI4=
-go4.org v0.0.0-20200411211856-f5505b9728dd h1:BNJlw5kRTzdmyfh5U8F93HA2OwkP7ZGwA51eJ/0wKOU=
-go4.org v0.0.0-20200411211856-f5505b9728dd/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZMZXMSBGhxRdsvzbkg=
+go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
+go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
+go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
+go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
+go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
+go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
+go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
+go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
+go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
+go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
 golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
-golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
-golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/arch v0.6.0 h1:S0JTfE48HbRj80+4tbvZDYsJ3tGv6BUU3XxyZ7CirAc=
+golang.org/x/arch v0.6.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
-golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
-golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
+golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
+golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
+golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
+golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
+golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
+golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
+golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -545,8 +602,9 @@ golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE
 golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
-golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
-golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
+golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
+golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 h1:qCEDpW1G+vcj3Y7Fy52pEM1AWm3abj8WimGYejI3SC4=
+golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -565,7 +623,10 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
 golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
+golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -583,26 +644,25 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
-golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.0.0-20220401154927-543a649e0bdd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
-golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos=
-golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
+golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
+golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
+golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
+golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
+golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
+golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -615,9 +675,11 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
-golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
+golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -640,36 +702,42 @@ golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
-golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
+golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
+golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
+golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -678,14 +746,18 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U=
-golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
+golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -714,9 +786,11 @@ golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapK
 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM=
+golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -748,29 +822,37 @@ google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvx
 google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
 google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
 google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
 google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
 google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
 google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
 google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
-google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
@@ -783,6 +865,7 @@ gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@@ -799,6 +882,7 @@ modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
 modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
 modernc.org/sqlite v1.21.1 h1:GyDFqNnESLOhwwDRaHGdp2jKLDzpyT/rNLglX3ZkMSU=
 modernc.org/sqlite v1.21.1/go.mod h1:XwQ0wZPIh1iKb5mkvCJ3szzbhk+tykC8ZWqTRTgYRwI=
+nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
 rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
diff --git a/src/config/default.go b/src/config/default.go
index 488d633..0d267b3 100644
--- a/src/config/default.go
+++ b/src/config/default.go
@@ -20,6 +20,10 @@ var defaultConfig = Config{
 		Fuse: Fuse{
 			Enabled: false,
 		},
+		NFS: NFS{
+			Enabled: false,
+			Port:    8122,
+		},
 	},
 
 	TorrentClient: TorrentClient{
diff --git a/src/config/load.go b/src/config/load.go
index 5ccb1d9..9f7a78e 100644
--- a/src/config/load.go
+++ b/src/config/load.go
@@ -42,7 +42,10 @@ func Load(path string) (*Config, error) {
 	}
 
 	conf := Config{}
-	k.Unmarshal("", &conf)
+	err = k.Unmarshal("", &conf)
+	if err != nil {
+		return nil, err
+	}
 
 	return &conf, nil
 }
diff --git a/src/config/model.go b/src/config/model.go
index f7c9fd9..d967ae8 100644
--- a/src/config/model.go
+++ b/src/config/model.go
@@ -61,6 +61,12 @@ type Mounts struct {
 	WebDAV WebDAV `koanf:"webdav"`
 	HttpFs HttpFs `koanf:"httpfs"`
 	Fuse   Fuse   `koanf:"fuse"`
+	NFS    NFS    `koanf:"nfs"`
+}
+
+type NFS struct {
+	Enabled bool `koanf:"enabled"`
+	Port    int  `koanf:"port"`
 }
 
 type HttpFs struct {
diff --git a/src/host/storage.go b/src/host/storage.go
index 8372068..c080d72 100644
--- a/src/host/storage.go
+++ b/src/host/storage.go
@@ -5,11 +5,7 @@ import (
 	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
 )
 
-type storage struct {
-	factories map[string]vfs.FsFactory
-}
-
-func NewStorage(downPath string, tsrv *torrent.Service) vfs.Filesystem {
+func NewStorage(dataPath string, tsrv *torrent.Service) vfs.Filesystem {
 	factories := map[string]vfs.FsFactory{
 		".torrent": tsrv.NewTorrentFs,
 	}
@@ -19,102 +15,5 @@ func NewStorage(downPath string, tsrv *torrent.Service) vfs.Filesystem {
 		factories[k] = v
 	}
 
-	return vfs.NewResolveFS(downPath, factories)
+	return vfs.NewResolveFS(vfs.NewOsFs(dataPath), factories)
 }
-
-// func (s *storage) Clear() {
-// 	s.files = make(map[string]vfs.File)
-// }
-
-// func (s *storage) Has(path string) bool {
-// 	path = clean(path)
-
-// 	f := s.files[path]
-// 	if f != nil {
-// 		return true
-// 	}
-
-// 	if f, _ := s.getFileFromFs(path); f != nil {
-// 		return true
-// 	}
-
-// 	return false
-// }
-
-// func (s *storage) createParent(p string, f File) error {
-// 	base, filename := path.Split(p)
-// 	base = clean(base)
-
-// 	if err := s.Add(&Dir{}, base); err != nil {
-// 		return err
-// 	}
-
-// 	if _, ok := s.children[base]; !ok {
-// 		s.children[base] = make(map[string]File)
-// 	}
-
-// 	if filename != "" {
-// 		s.children[base][filename] = f
-// 	}
-
-// 	return nil
-// }
-
-// func (s *storage) Children(path string) (map[string]File, error) {
-// 	path = clean(path)
-
-// 	files, err := s.getDirFromFs(path)
-// 	if err == nil {
-// 		return files, nil
-// 	}
-
-// 	if !os.IsNotExist(err) {
-// 		return nil, err
-// 	}
-
-// 	l := make(map[string]File)
-// 	for n, f := range s.children[path] {
-// 		l[n] = f
-// 	}
-
-// 	return l, nil
-// }
-
-// func (s *storage) Get(path string) (File, error) {
-// 	path = clean(path)
-// 	if !s.Has(path) {
-// 		return nil, os.ErrNotExist
-// 	}
-
-// 	file, ok := s.files[path]
-// 	if ok {
-// 		return file, nil
-// 	}
-
-// 	return s.getFileFromFs(path)
-// }
-
-// func (s *storage) getFileFromFs(p string) (File, error) {
-// 	for fsp, fs := range s.filesystems {
-// 		if strings.HasPrefix(p, fsp) {
-// 			return fs.Open(separator + strings.TrimPrefix(p, fsp))
-// 		}
-// 	}
-
-// 	return nil, os.ErrNotExist
-// }
-
-// func (s *storage) getDirFromFs(p string) (map[string]File, error) {
-// 	for fsp, fs := range s.filesystems {
-// 		if strings.HasPrefix(p, fsp) {
-// 			path := strings.TrimPrefix(p, fsp)
-// 			return fs.ReadDir(path)
-// 		}
-// 	}
-
-// 	return nil, os.ErrNotExist
-// }
-
-// func clean(p string) string {
-// 	return path.Clean(separator + strings.ReplaceAll(p, "\\", "/"))
-// }
diff --git a/src/host/torrent/client.go b/src/host/torrent/client.go
index f0936d7..d432883 100644
--- a/src/host/torrent/client.go
+++ b/src/host/torrent/client.go
@@ -17,10 +17,22 @@ import (
 func NewClient(st storage.ClientImpl, fis bep44.Store, cfg *config.TorrentClient, id [20]byte) (*torrent.Client, error) {
 	// TODO download and upload limits
 	torrentCfg := torrent.NewDefaultClientConfig()
-	torrentCfg.Seed = true
 	torrentCfg.PeerID = string(id[:])
 	torrentCfg.DefaultStorage = st
-	torrentCfg.DisableIPv6 = cfg.DisableIPv6
+
+	// torrentCfg.DisableIPv6 = cfg.DisableIPv6
+	// torrentCfg.DropDuplicatePeerIds = true
+	// torrentCfg.TorrentPeersLowWater = 10
+	// torrentCfg.TorrentPeersHighWater = 100
+	// torrentCfg.DisableWebtorrent = true
+	// torrentCfg.DisableAggressiveUpload = true
+	// torrentCfg.DisableWebseeds = true
+	// torrentCfg.DisableUTP = false
+	// torrentCfg.NoDefaultPortForwarding = true
+	// torrentCfg.AlwaysWantConns = false
+	// torrentCfg.ClientDhtConfig = torrent.ClientDhtConfig{
+	// 	NoDHT: true,
+	// }
 
 	l := log.Logger.With().Str("component", "torrent-client").Logger()
 
diff --git a/src/host/torrent/piece-completion.go b/src/host/torrent/piece-completion.go
new file mode 100644
index 0000000..4b7eeec
--- /dev/null
+++ b/src/host/torrent/piece-completion.go
@@ -0,0 +1,131 @@
+package torrent
+
+import (
+	"encoding/binary"
+	"fmt"
+	"log/slog"
+
+	"github.com/anacrolix/torrent/metainfo"
+	"github.com/anacrolix/torrent/storage"
+	"github.com/dgraph-io/badger/v4"
+)
+
+type PieceCompletionState byte
+
+const (
+	PieceNotComplete PieceCompletionState = 0
+	PieceComplete    PieceCompletionState = 1<<8 - 1
+)
+
+func pieceCompletionState(i bool) PieceCompletionState {
+	if i {
+		return PieceComplete
+	} else {
+		return PieceNotComplete
+	}
+}
+
+type badgerPieceCompletion struct {
+	db *badger.DB
+}
+
+var _ storage.PieceCompletion = (*badgerPieceCompletion)(nil)
+
+func NewBadgerPieceCompletion(dir string) (storage.PieceCompletion, error) {
+	opts := badger.
+		DefaultOptions(dir).
+		WithLogger(badgerSlog{slog: slog.With("component", "piece-completion")})
+	db, err := badger.Open(opts)
+	if err != nil {
+		return nil, err
+	}
+	return &badgerPieceCompletion{db}, nil
+}
+
+func pkToBytes(pk metainfo.PieceKey) []byte {
+	key := make([]byte, len(pk.InfoHash.Bytes()))
+	copy(key, pk.InfoHash.Bytes())
+	binary.BigEndian.AppendUint32(key, uint32(pk.Index))
+	return key
+}
+
+func (k *badgerPieceCompletion) Get(pk metainfo.PieceKey) (storage.Completion, error) {
+	completion := storage.Completion{
+		Ok: true,
+	}
+	err := k.db.View(func(tx *badger.Txn) error {
+		item, err := tx.Get(pkToBytes(pk))
+		if err != nil {
+			if err == badger.ErrKeyNotFound {
+				completion.Ok = false
+				return nil
+			}
+
+			return fmt.Errorf("getting value: %w", err)
+		}
+
+		valCopy, err := item.ValueCopy(nil)
+		if err != nil {
+			return fmt.Errorf("copying value: %w", err)
+		}
+		compl := PieceCompletionState(valCopy[0])
+
+		completion.Ok = true
+		switch compl {
+		case PieceComplete:
+			completion.Complete = true
+		case PieceNotComplete:
+			completion.Complete = false
+		}
+
+		return nil
+	})
+	return completion, err
+}
+
+func (me badgerPieceCompletion) Set(pk metainfo.PieceKey, b bool) error {
+	if c, err := me.Get(pk); err == nil && c.Ok && c.Complete == b {
+		return nil
+	}
+
+	return me.db.Update(func(txn *badger.Txn) error {
+		return txn.Set(pkToBytes(pk), []byte{byte(pieceCompletionState(b))})
+	})
+}
+
+func (k *badgerPieceCompletion) Delete(key string) error {
+	return k.db.Update(
+		func(txn *badger.Txn) error {
+			return txn.Delete([]byte(key))
+		})
+}
+
+func (me *badgerPieceCompletion) Close() error {
+	return me.db.Close()
+}
+
+type badgerSlog struct {
+	slog *slog.Logger
+}
+
+// Debugf implements badger.Logger.
+func (log badgerSlog) Debugf(f string, a ...interface{}) {
+	log.slog.Debug(f, a...)
+}
+
+// Errorf implements badger.Logger.
+func (log badgerSlog) Errorf(f string, a ...interface{}) {
+	log.slog.Error(f, a...)
+}
+
+// Infof implements badger.Logger.
+func (log badgerSlog) Infof(f string, a ...interface{}) {
+	log.slog.Info(f, a...)
+}
+
+// Warningf implements badger.Logger.
+func (log badgerSlog) Warningf(f string, a ...interface{}) {
+	log.slog.Warn(f, a...)
+}
+
+var _ badger.Logger = (*badgerSlog)(nil)
diff --git a/src/host/torrent/service.go b/src/host/torrent/service.go
index b251aff..7a074de 100644
--- a/src/host/torrent/service.go
+++ b/src/host/torrent/service.go
@@ -1,31 +1,33 @@
 package torrent
 
 import (
-	"sync"
+	"context"
+	"fmt"
+	"log/slog"
+	"time"
 
 	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
 	"github.com/anacrolix/torrent"
 	"github.com/anacrolix/torrent/metainfo"
-	"github.com/rs/zerolog"
-	"github.com/rs/zerolog/log"
+	"github.com/anacrolix/torrent/types"
 )
 
 type Service struct {
 	c *torrent.Client
 
 	// stats *Stats
+	DefaultPriority types.PiecePriority
 
-	mu sync.Mutex
-
-	log                     zerolog.Logger
+	log                     *slog.Logger
 	addTimeout, readTimeout int
 }
 
 func NewService(c *torrent.Client, addTimeout, readTimeout int) *Service {
-	l := log.Logger.With().Str("component", "torrent-service").Logger()
+	l := slog.With("component", "torrent-service")
 	return &Service{
-		log: l,
-		c:   c,
+		log:             l,
+		c:               c,
+		DefaultPriority: types.PiecePriorityNone,
 		// stats:       newStats(), // TODO persistent
 		addTimeout:  addTimeout,
 		readTimeout: readTimeout,
@@ -35,20 +37,30 @@ func NewService(c *torrent.Client, addTimeout, readTimeout int) *Service {
 var _ vfs.FsFactory = (*Service)(nil).NewTorrentFs
 
 func (s *Service) NewTorrentFs(f vfs.File) (vfs.Filesystem, error) {
+	ctx, cancel := context.WithTimeout(context.TODO(), time.Second*time.Duration(s.addTimeout))
+	defer cancel()
 	defer f.Close()
 
 	mi, err := metainfo.Load(f)
 	if err != nil {
 		return nil, err
 	}
-	t, err := s.c.AddTorrent(mi)
-	if err != nil {
-		return nil, err
-	}
-	<-t.GotInfo()
-	t.AllowDataDownload()
-	for _, f := range t.Files() {
-		f.SetPriority(torrent.PiecePriorityReadahead)
+
+	t, ok := s.c.Torrent(mi.HashInfoBytes())
+	if !ok {
+		t, err = s.c.AddTorrent(mi)
+		if err != nil {
+			return nil, err
+		}
+		select {
+		case <-ctx.Done():
+			return nil, fmt.Errorf("creating torrent fs timed out")
+		case <-t.GotInfo():
+		}
+		for _, f := range t.Files() {
+			f.SetPriority(s.DefaultPriority)
+		}
+		t.AllowDataDownload()
 	}
 
 	return vfs.NewTorrentFs(t, s.readTimeout), nil
@@ -57,161 +69,3 @@ func (s *Service) NewTorrentFs(f vfs.File) (vfs.Filesystem, error) {
 func (s *Service) Stats() (*Stats, error) {
 	return &Stats{}, nil
 }
-
-// func (s *Service) Load() (map[string]vfs.Filesystem, error) {
-// 	// Load from config
-// 	s.log.Info().Msg("adding torrents from configuration")
-// 	for _, loader := range s.loaders {
-// 		if err := s.load(loader); err != nil {
-// 			return nil, err
-// 		}
-// 	}
-
-// 	// Load from DB
-// 	s.log.Info().Msg("adding torrents from database")
-// 	return s.fss, s.load(s.db)
-// }
-
-// func (s *Service) load(l loader.Loader) error {
-// 	list, err := l.ListMagnets()
-// 	if err != nil {
-// 		return err
-// 	}
-// 	for r, ms := range list {
-// 		s.addRoute(r)
-// 		for _, m := range ms {
-// 			if err := s.addMagnet(r, m); err != nil {
-// 				return err
-// 			}
-// 		}
-// 	}
-
-// 	list, err = l.ListTorrentPaths()
-// 	if err != nil {
-// 		return err
-// 	}
-// 	for r, ms := range list {
-// 		s.addRoute(r)
-// 		for _, p := range ms {
-// 			if err := s.addTorrentPath(r, p); err != nil {
-// 				return err
-// 			}
-// 		}
-// 	}
-
-// 	return nil
-// }
-
-// func (s *Service) AddMagnet(r, m string) error {
-// 	if err := s.addMagnet(r, m); err != nil {
-// 		return err
-// 	}
-
-// 	// Add to db
-// 	return s.db.AddMagnet(r, m)
-// }
-
-// func (s *Service) addTorrentPath(r, p string) error {
-// 	// Add to client
-// 	t, err := s.c.AddTorrentFromFile(p)
-// 	if err != nil {
-// 		return err
-// 	}
-
-// 	return s.addTorrent(r, t)
-// }
-
-// func (s *Service) addMagnet(r, m string) error {
-// 	// Add to client
-// 	t, err := s.c.AddMagnet(m)
-// 	if err != nil {
-// 		return err
-// 	}
-
-// 	return s.addTorrent(r, t)
-
-// }
-
-// func (s *Service) addRoute(r string) {
-// 	s.s.AddRoute(r)
-
-// 	// Add to filesystems
-// 	folder := path.Join("/", r)
-// 	s.mu.Lock()
-// 	defer s.mu.Unlock()
-// 	_, ok := s.fss[folder]
-// 	if !ok {
-// 		s.fss[folder] = vfs.NewTorrentFs(s.readTimeout)
-// 	}
-// }
-
-// func (s *Service) addTorrent(r string, t *torrent.Torrent) error {
-// 	// only get info if name is not available
-// 	if t.Info() == nil {
-// 		s.log.Info().Str("hash", t.InfoHash().String()).Msg("getting torrent info")
-// 		select {
-// 		case <-time.After(time.Duration(s.addTimeout) * time.Second):
-// 			s.log.Error().Str("hash", t.InfoHash().String()).Msg("timeout getting torrent info")
-// 			return errors.New("timeout getting torrent info")
-// 		case <-t.GotInfo():
-// 			s.log.Info().Str("hash", t.InfoHash().String()).Msg("obtained torrent info")
-// 		}
-
-// 	}
-
-// 	// Add to stats
-// 	s.s.Add(r, t)
-
-// 	// Add to filesystems
-// 	folder := path.Join("/", r)
-// 	s.mu.Lock()
-// 	defer s.mu.Unlock()
-
-// 	tfs, ok := s.fss[folder].(*vfs.TorrentFs)
-// 	if !ok {
-// 		return errors.New("error adding torrent to filesystem")
-// 	}
-
-// 	tfs.AddTorrent(t)
-// 	s.log.Info().Str("name", t.Info().Name).Str("route", r).Msg("torrent added")
-
-// 	return nil
-// }
-
-// func (s *Service) RemoveFromHash(r, h string) error {
-// 	// Remove from db
-// 	deleted, err := s.db.RemoveFromHash(r, h)
-// 	if err != nil {
-// 		return err
-// 	}
-
-// 	if !deleted {
-// 		return fmt.Errorf("element with hash %v on route %v cannot be removed", h, r)
-// 	}
-
-// 	// Remove from stats
-// 	s.s.Del(r, h)
-
-// 	// Remove from fs
-// 	folder := path.Join("/", r)
-
-// 	tfs, ok := s.fss[folder].(*vfs.TorrentFs)
-// 	if !ok {
-// 		return errors.New("error removing torrent from filesystem")
-// 	}
-
-// 	tfs.RemoveTorrent(h)
-
-// 	// Remove from client
-// 	var mh metainfo.Hash
-// 	if err := mh.FromHexString(h); err != nil {
-// 		return err
-// 	}
-
-// 	t, ok := s.c.Torrent(metainfo.NewHashFromHex(h))
-// 	if ok {
-// 		t.Drop()
-// 	}
-
-// 	return nil
-// }
diff --git a/src/host/torrent/storage.go b/src/host/torrent/storage.go
new file mode 100644
index 0000000..24f694a
--- /dev/null
+++ b/src/host/torrent/storage.go
@@ -0,0 +1,306 @@
+package torrent
+
+import (
+	"errors"
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+
+	"git.kmsign.ru/royalcat/tstor/src/config"
+	"github.com/anacrolix/missinggo"
+	"github.com/anacrolix/torrent"
+	"github.com/anacrolix/torrent/metainfo"
+	"github.com/anacrolix/torrent/mmap_span"
+	"github.com/anacrolix/torrent/storage"
+	"github.com/edsrzf/mmap-go"
+)
+
+type Torrent struct {
+	client *torrent.Client
+	data   storage.ClientImplCloser
+	pc     storage.PieceCompletion
+}
+
+func SetupStorage(cfg config.TorrentClient) (storage.ClientImplCloser, storage.PieceCompletion, error) {
+	pcp := filepath.Join(cfg.DataFolder, "piece-completion")
+	if err := os.MkdirAll(pcp, 0744); err != nil {
+		return nil, nil, fmt.Errorf("error creating piece completion folder: %w", err)
+	}
+	pc, err := storage.NewBoltPieceCompletion(pcp)
+	if err != nil {
+		return nil, nil, fmt.Errorf("error creating servers piece completion: %w", err)
+	}
+
+	// pc, err := NewBadgerPieceCompletion(pcp)
+	// if err != nil {
+	// 	return nil, nil, fmt.Errorf("error creating servers piece completion: %w", err)
+	// }
+
+	// TODO implement cache/storage switching
+	// cacheDir := filepath.Join(tcfg.DataFolder, "cache")
+	// if err := os.MkdirAll(cacheDir, 0744); err != nil {
+	// 	return nil, nil, fmt.Errorf("error creating piece completion folder: %w", err)
+	// }
+	// fc, err := filecache.NewCache(cacheDir)
+	// if err != nil {
+	// 	return nil, nil, fmt.Errorf("error creating cache: %w", err)
+	// }
+	// log.Info().Msg(fmt.Sprintf("setting cache size to %d MB", 1024))
+	// fc.SetCapacity(1024 * 1024 * 1024)
+
+	// rp := storage.NewResourcePieces(fc.AsResourceProvider())
+	// st := &stc{rp}
+
+	filesDir := filepath.Join(cfg.DataFolder, "files")
+	if err := os.MkdirAll(pcp, 0744); err != nil {
+		return nil, nil, fmt.Errorf("error creating piece completion folder: %w", err)
+	}
+
+	// st := storage.NewMMapWithCompletion(filesDir, pc)
+	st := storage.NewFileOpts(storage.NewFileClientOpts{
+		ClientBaseDir:   filesDir,
+		PieceCompletion: pc,
+	})
+
+	return st, pc, nil
+}
+
+func (s Torrent) Remove(f *torrent.File) error {
+
+	return nil
+}
+
+// type dupePieces struct {
+// }
+
+// func (s Torrent) dedupe(f1, f2 *os.File) error {
+// 	for _, t := range s.client.Torrents() {
+// 		for i := 0; i < t.NumPieces(); i++ {
+// 			p := t.Piece(i)
+// 			p.Info().Hash()
+// 		}
+// 	}
+
+// 	// https://go-review.googlesource.com/c/sys/+/284352/10/unix/syscall_linux_test.go#856
+// 	// dedupe := unix.FileDedupeRange{
+// 	// 	Src_offset: uint64(0),
+// 	// 	Src_length: uint64(4096),
+// 	// 	Info: []unix.FileDedupeRangeInfo{
+// 	// 		unix.FileDedupeRangeInfo{
+// 	// 			Dest_fd:     int64(f2.Fd()),
+// 	// 			Dest_offset: uint64(0),
+// 	// 		},
+// 	// 		unix.FileDedupeRangeInfo{
+// 	// 			Dest_fd:     int64(f2.Fd()),
+// 	// 			Dest_offset: uint64(4096),
+// 	// 		},
+// 	// 	}}
+// 	// err := unix.IoctlFileDedupeRange(int(f1.Fd()), &dedupe)
+// 	// if err == unix.EOPNOTSUPP || err == unix.EINVAL {
+// 	// 	t.Skip("deduplication not supported on this filesystem")
+// 	// } else if err != nil {
+// 	// 	t.Fatal(err)
+// 	// }
+
+// 	return nil
+// }
+
+type mmapClientImpl struct {
+	baseDir string
+	pc      storage.PieceCompletion
+}
+
+func NewMMapWithCompletion(baseDir string, completion storage.PieceCompletion) *mmapClientImpl {
+	return &mmapClientImpl{
+		baseDir: baseDir,
+		pc:      completion,
+	}
+}
+
+func (s *mmapClientImpl) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (_ storage.TorrentImpl, err error) {
+	t, err := newMMapTorrent(info, infoHash, s.baseDir, s.pc)
+	if err != nil {
+		return storage.TorrentImpl{}, err
+	}
+	return storage.TorrentImpl{Piece: t.Piece, Close: t.Close, Flush: t.Flush}, nil
+}
+
+func (s *mmapClientImpl) Close() error {
+	return s.pc.Close()
+}
+
+func newMMapTorrent(md *metainfo.Info, infoHash metainfo.Hash, location string, pc storage.PieceCompletionGetSetter) (*mmapTorrent, error) {
+	span := &mmap_span.MMapSpan{}
+	basePath, err := storage.ToSafeFilePath(md.Name)
+	if err != nil {
+		return nil, err
+	}
+	basePath = filepath.Join(location, basePath)
+
+	for _, miFile := range md.UpvertedFiles() {
+		var safeName string
+		safeName, err = storage.ToSafeFilePath(miFile.Path...)
+		if err != nil {
+			return nil, err
+		}
+		fileName := filepath.Join(basePath, safeName)
+		var mm FileMapping
+		mm, err = mmapFile(fileName, miFile.Length)
+		if err != nil {
+			err = fmt.Errorf("file %q: %s", miFile.DisplayPath(md), err)
+			return nil, err
+		}
+		span.Append(mm)
+	}
+	span.InitIndex()
+
+	return &mmapTorrent{
+		infoHash: infoHash,
+		span:     span,
+		pc:       pc,
+	}, nil
+}
+
+type mmapTorrent struct {
+	infoHash metainfo.Hash
+	span     *mmap_span.MMapSpan
+	pc       storage.PieceCompletionGetSetter
+}
+
+func (ts *mmapTorrent) Piece(p metainfo.Piece) storage.PieceImpl {
+	return mmapPiece{
+		pc:       ts.pc,
+		p:        p,
+		ih:       ts.infoHash,
+		ReaderAt: io.NewSectionReader(ts.span, p.Offset(), p.Length()),
+		WriterAt: missinggo.NewSectionWriter(ts.span, p.Offset(), p.Length()),
+	}
+}
+
+func (ts *mmapTorrent) Close() error {
+	errs := ts.span.Close()
+	if len(errs) > 0 {
+		return errs[0]
+	}
+	return nil
+}
+
+func (ts *mmapTorrent) Flush() error {
+	errs := ts.span.Flush()
+	if len(errs) > 0 {
+		return errs[0]
+	}
+	return nil
+}
+
+type mmapPiece struct {
+	pc storage.PieceCompletionGetSetter
+	p  metainfo.Piece
+	ih metainfo.Hash
+	io.ReaderAt
+	io.WriterAt
+}
+
+func (me mmapPiece) pieceKey() metainfo.PieceKey {
+	return metainfo.PieceKey{InfoHash: me.ih, Index: me.p.Index()}
+}
+
+func (sp mmapPiece) Completion() storage.Completion {
+	c, err := sp.pc.Get(sp.pieceKey())
+	if err != nil {
+		panic(err)
+	}
+	return c
+}
+
+func (sp mmapPiece) MarkComplete() error {
+	return sp.pc.Set(sp.pieceKey(), true)
+}
+
+func (sp mmapPiece) MarkNotComplete() error {
+	return sp.pc.Set(sp.pieceKey(), false)
+}
+
+func mmapFile(name string, size int64) (_ FileMapping, err error) {
+	dir := filepath.Dir(name)
+	err = os.MkdirAll(dir, 0o750)
+	if err != nil {
+		return nil, fmt.Errorf("making directory %q: %s", dir, err)
+	}
+	var file *os.File
+	file, err = os.OpenFile(name, os.O_CREATE|os.O_RDWR, 0o666)
+	if err != nil {
+		return nil, err
+	}
+	defer func() {
+		if err != nil {
+			file.Close()
+		}
+	}()
+	var fi os.FileInfo
+	fi, err = file.Stat()
+	if err != nil {
+		return nil, err
+	}
+	if fi.Size() < size {
+		// I think this is necessary on HFS+. Maybe Linux will SIGBUS too if
+		// you overmap a file but I'm not sure.
+		err = file.Truncate(size)
+		if err != nil {
+			return nil, err
+		}
+	}
+	return func() (ret mmapWithFile, err error) {
+		ret.f = file
+		if size == 0 {
+			// Can't mmap() regions with length 0.
+			return
+		}
+		intLen := int(size)
+		if int64(intLen) != size {
+			err = errors.New("size too large for system")
+			return
+		}
+		ret.mmap, err = mmap.MapRegion(file, intLen, mmap.RDWR, 0, 0)
+		if err != nil {
+			err = fmt.Errorf("error mapping region: %s", err)
+			return
+		}
+		if int64(len(ret.mmap)) != size {
+			panic(len(ret.mmap))
+		}
+		return
+	}()
+}
+
+type FileMapping = mmap_span.Mmap
+
+// Handles closing the mmap's file handle (needed for Windows). Could be implemented differently by
+// OS.
+type mmapWithFile struct {
+	f    *os.File
+	mmap mmap.MMap
+}
+
+func (m mmapWithFile) Flush() error {
+	return m.mmap.Flush()
+}
+
+func (m mmapWithFile) Unmap() (err error) {
+	if m.mmap != nil {
+		err = m.mmap.Unmap()
+	}
+	fileErr := m.f.Close()
+	if err == nil {
+		err = fileErr
+	}
+	return
+}
+
+func (m mmapWithFile) Bytes() []byte {
+	if m.mmap == nil {
+		return nil
+	}
+	return m.mmap
+}
diff --git a/src/host/vfs/archive.go b/src/host/vfs/archive.go
index 3963689..585f7f9 100644
--- a/src/host/vfs/archive.go
+++ b/src/host/vfs/archive.go
@@ -3,8 +3,11 @@ package vfs
 import (
 	"archive/zip"
 	"io"
+	"io/fs"
 	"os"
+	"path"
 	"path/filepath"
+	"strings"
 	"sync"
 
 	"git.kmsign.ru/royalcat/tstor/src/iio"
@@ -46,6 +49,8 @@ func NewArchive(r iio.Reader, size int64, loader ArchiveLoader) *archive {
 	}
 }
 
+var _ Filesystem = &archive{}
+
 func (a *archive) Open(filename string) (File, error) {
 	files, err := a.files()
 	if err != nil {
@@ -55,28 +60,56 @@ func (a *archive) Open(filename string) (File, error) {
 	return getFile(files, filename)
 }
 
-func (fs *archive) ReadDir(path string) (map[string]File, error) {
+func (fs *archive) ReadDir(path string) ([]fs.DirEntry, error) {
 	files, err := fs.files()
 	if err != nil {
 		return nil, err
 	}
 
-	return listFilesInDir(files, path)
+	return listDirFromFiles(files, path)
+}
+
+// Stat implements Filesystem.
+func (afs *archive) Stat(filename string) (fs.FileInfo, error) {
+	files, err := afs.files()
+	if err != nil {
+		return nil, err
+	}
+
+	if file, ok := files[filename]; ok {
+		return newFileInfo(path.Base(filename), file.Size()), nil
+	}
+
+	for p, _ := range files {
+		if strings.HasPrefix(p, filename) {
+			return newDirInfo(path.Base(filename)), nil
+		}
+	}
+
+	return nil, ErrNotExist
+
 }
 
 var _ File = &archiveFile{}
 
-func NewArchiveFile(readerFunc func() (iio.Reader, error), len int64) *archiveFile {
+func NewArchiveFile(name string, readerFunc func() (iio.Reader, error), size int64) *archiveFile {
 	return &archiveFile{
+		name:       name,
 		readerFunc: readerFunc,
-		len:        len,
+		size:       size,
 	}
 }
 
 type archiveFile struct {
+	name string
+
 	readerFunc func() (iio.Reader, error)
 	reader     iio.Reader
-	len        int64
+	size       int64
+}
+
+func (d *archiveFile) Stat() (fs.FileInfo, error) {
+	return newFileInfo(d.name, d.size), nil
 }
 
 func (d *archiveFile) load() error {
@@ -94,7 +127,7 @@ func (d *archiveFile) load() error {
 }
 
 func (d *archiveFile) Size() int64 {
-	return d.len
+	return d.size
 }
 
 func (d *archiveFile) IsDir() bool {
@@ -151,7 +184,7 @@ func ZipLoader(reader iio.Reader, size int64) (map[string]*archiveFile, error) {
 		}
 
 		n := filepath.Join(string(os.PathSeparator), f.Name)
-		af := NewArchiveFile(rf, f.FileInfo().Size())
+		af := NewArchiveFile(f.Name, rf, f.FileInfo().Size())
 
 		out[n] = af
 	}
@@ -183,7 +216,7 @@ func SevenZipLoader(reader iio.Reader, size int64) (map[string]*archiveFile, err
 			return iio.NewDiskTeeReader(zr)
 		}
 
-		af := NewArchiveFile(rf, f.FileInfo().Size())
+		af := NewArchiveFile(f.Name, rf, f.FileInfo().Size())
 		n := filepath.Join(string(os.PathSeparator), f.Name)
 
 		out[n] = af
@@ -216,7 +249,7 @@ func RarLoader(reader iio.Reader, size int64) (map[string]*archiveFile, error) {
 
 		n := filepath.Join(string(os.PathSeparator), header.Name)
 
-		af := NewArchiveFile(rf, header.UnPackedSize)
+		af := NewArchiveFile(header.Name, rf, header.UnPackedSize)
 
 		out[n] = af
 	}
diff --git a/src/host/vfs/archive_test.go b/src/host/vfs/archive_test.go
index 0383b56..a984d92 100644
--- a/src/host/vfs/archive_test.go
+++ b/src/host/vfs/archive_test.go
@@ -16,18 +16,21 @@ func TestZipFilesystem(t *testing.T) {
 	t.Parallel()
 	require := require.New(t)
 
-	zReader, len := createTestZip(require)
+	zReader, size := createTestZip(require)
 
-	zfs := NewArchive(zReader, len, ZipLoader)
+	zfs := NewArchive(zReader, size, ZipLoader)
 
 	files, err := zfs.ReadDir("/path/to/test/file")
 	require.NoError(err)
 
 	require.Len(files, 1)
-	f := files["1.txt"]
-	require.NotNil(f)
+	e := files[0]
+	require.Equal("1.txt", e.Name())
+	require.NotNil(e)
 
 	out := make([]byte, 11)
+	f, err := zfs.Open("/path/to/test/file/1.txt")
+	require.NoError(err)
 	n, err := f.Read(out)
 	require.Equal(io.EOF, err)
 	require.Equal(11, n)
diff --git a/src/host/vfs/dir.go b/src/host/vfs/dir.go
index 3077321..fc0c1a0 100644
--- a/src/host/vfs/dir.go
+++ b/src/host/vfs/dir.go
@@ -1,26 +1,43 @@
 package vfs
 
-var _ File = &Dir{}
+import (
+	"io/fs"
+	"path"
+)
 
-type Dir struct {
+var _ File = &dir{}
+
+func NewDir(name string) File {
+	return &dir{
+		name: path.Base(name),
+	}
 }
 
-func (d *Dir) Size() int64 {
+type dir struct {
+	name string
+}
+
+// Info implements File.
+func (d *dir) Stat() (fs.FileInfo, error) {
+	return newDirInfo(d.name), nil
+}
+
+func (d *dir) Size() int64 {
 	return 0
 }
 
-func (d *Dir) IsDir() bool {
+func (d *dir) IsDir() bool {
 	return true
 }
 
-func (d *Dir) Close() error {
+func (d *dir) Close() error {
 	return nil
 }
 
-func (d *Dir) Read(p []byte) (n int, err error) {
+func (d *dir) Read(p []byte) (n int, err error) {
 	return 0, nil
 }
 
-func (d *Dir) ReadAt(p []byte, off int64) (n int, err error) {
+func (d *dir) ReadAt(p []byte, off int64) (n int, err error) {
 	return 0, nil
 }
diff --git a/src/host/vfs/fs.go b/src/host/vfs/fs.go
index 8a73aad..3cfc1fc 100644
--- a/src/host/vfs/fs.go
+++ b/src/host/vfs/fs.go
@@ -1,7 +1,9 @@
 package vfs
 
 import (
-	"os"
+	"errors"
+	"io/fs"
+	"path"
 	"time"
 
 	"git.kmsign.ru/royalcat/tstor/src/iio"
@@ -10,10 +12,13 @@ import (
 type File interface {
 	IsDir() bool
 	Size() int64
+	Stat() (fs.FileInfo, error)
 
 	iio.Reader
 }
 
+var ErrNotImplemented = errors.New("not implemented")
+
 type Filesystem interface {
 	// Open opens the named file for reading. If successful, methods on the
 	// returned file can be used for reading; the associated file descriptor has
@@ -22,23 +27,50 @@ type Filesystem interface {
 
 	// ReadDir reads the directory named by dirname and returns a list of
 	// directory entries.
-	ReadDir(path string) (map[string]File, error)
+	ReadDir(path string) ([]fs.DirEntry, error)
+
+	Stat(filename string) (fs.FileInfo, error)
 }
 
+const defaultMode = fs.FileMode(0555)
+
 type fileInfo struct {
 	name  string
 	size  int64
 	isDir bool
 }
 
-func NewFileInfo(name string, size int64, isDir bool) *fileInfo {
+var _ fs.FileInfo = &fileInfo{}
+var _ fs.DirEntry = &fileInfo{}
+
+func newDirInfo(name string) *fileInfo {
 	return &fileInfo{
-		name:  name,
-		size:  size,
-		isDir: isDir,
+		name:  path.Base(name),
+		size:  0,
+		isDir: true,
 	}
 }
 
+func newFileInfo(name string, size int64) *fileInfo {
+	return &fileInfo{
+		name:  path.Base(name),
+		size:  size,
+		isDir: false,
+	}
+}
+
+func (fi *fileInfo) Info() (fs.FileInfo, error) {
+	return fi, nil
+}
+
+func (fi *fileInfo) Type() fs.FileMode {
+	if fi.isDir {
+		return fs.ModeDir
+	}
+
+	return 0
+}
+
 func (fi *fileInfo) Name() string {
 	return fi.name
 }
@@ -47,17 +79,17 @@ func (fi *fileInfo) Size() int64 {
 	return fi.size
 }
 
-func (fi *fileInfo) Mode() os.FileMode {
+func (fi *fileInfo) Mode() fs.FileMode {
 	if fi.isDir {
-		return 0555 | os.ModeDir
+		return defaultMode | fs.ModeDir
 	}
 
-	return 0555
+	return defaultMode
 }
 
 func (fi *fileInfo) ModTime() time.Time {
 	// TODO fix it
-	return time.Now()
+	return time.Time{}
 }
 
 func (fi *fileInfo) IsDir() bool {
diff --git a/src/host/vfs/fs_test.go b/src/host/vfs/fs_test.go
index 8a79fdd..414f556 100644
--- a/src/host/vfs/fs_test.go
+++ b/src/host/vfs/fs_test.go
@@ -12,13 +12,32 @@ func TestFileinfo(t *testing.T) {
 
 	require := require.New(t)
 
-	fi := NewFileInfo("name", 42, false)
+	fi := newFileInfo("abc/name", 42)
 
-	require.Equal(fi.IsDir(), false)
-	require.Equal(fi.Name(), "name")
-	require.Equal(fi.Size(), int64(42))
+	require.Equal("name", fi.Name())
+	require.False(fi.IsDir())
+	require.Equal(int64(42), fi.Size())
 	require.NotNil(fi.ModTime())
-	require.Equal(fi.Mode(), fs.FileMode(0555))
-	require.Equal(fi.Sys(), nil)
+	require.Zero(fi.Type() & fs.ModeDir)
+	require.Zero(fi.Mode() & fs.ModeDir)
+	require.Equal(fs.FileMode(0555), fi.Mode())
+	require.Equal(nil, fi.Sys())
+}
+
+func TestDirInfo(t *testing.T) {
+	t.Parallel()
+
+	require := require.New(t)
+
+	fi := newDirInfo("abc/name")
+
+	require.True(fi.IsDir())
+	require.Equal("name", fi.Name())
+	require.Equal(int64(0), fi.Size())
+	require.NotNil(fi.ModTime())
+	require.NotZero(fi.Type() & fs.ModeDir)
+	require.NotZero(fi.Mode() & fs.ModeDir)
+	require.Equal(defaultMode|fs.ModeDir, fi.Mode())
+	require.Equal(nil, fi.Sys())
 
 }
diff --git a/src/host/vfs/memory.go b/src/host/vfs/memory.go
index 1031a7d..eada6e0 100644
--- a/src/host/vfs/memory.go
+++ b/src/host/vfs/memory.go
@@ -2,6 +2,8 @@ package vfs
 
 import (
 	"bytes"
+	"io/fs"
+	"path"
 )
 
 var _ Filesystem = &MemoryFs{}
@@ -20,22 +22,37 @@ func (m *MemoryFs) Open(filename string) (File, error) {
 	return getFile(m.files, filename)
 }
 
-func (fs *MemoryFs) ReadDir(path string) (map[string]File, error) {
-	return listFilesInDir(fs.files, path)
+func (fs *MemoryFs) ReadDir(path string) ([]fs.DirEntry, error) {
+	return listDirFromFiles(fs.files, path)
+}
+
+// Stat implements Filesystem.
+func (mfs *MemoryFs) Stat(filename string) (fs.FileInfo, error) {
+	file, ok := mfs.files[filename]
+	if !ok {
+		return nil, ErrNotExist
+	}
+	return newFileInfo(path.Base(filename), file.Size()), nil
 }
 
 var _ File = &MemoryFile{}
 
 type MemoryFile struct {
+	name string
 	*bytes.Reader
 }
 
-func NewMemoryFile(data []byte) *MemoryFile {
+func NewMemoryFile(name string, data []byte) *MemoryFile {
 	return &MemoryFile{
+		name:   name,
 		Reader: bytes.NewReader(data),
 	}
 }
 
+func (d *MemoryFile) Stat() (fs.FileInfo, error) {
+	return newFileInfo(d.name, int64(d.Reader.Len())), nil
+}
+
 func (d *MemoryFile) Size() int64 {
 	return int64(d.Reader.Len())
 }
diff --git a/src/host/vfs/memory_test.go b/src/host/vfs/memory_test.go
index 9d090d8..224c8ef 100644
--- a/src/host/vfs/memory_test.go
+++ b/src/host/vfs/memory_test.go
@@ -13,7 +13,7 @@ func TestMemory(t *testing.T) {
 	testData := "Hello"
 
 	c := NewMemoryFS(map[string]*MemoryFile{
-		"/dir/here": NewMemoryFile([]byte(testData)),
+		"/dir/here": NewMemoryFile("here", []byte(testData)),
 	})
 
 	// fss := map[string]Filesystem{
@@ -32,7 +32,7 @@ func TestMemory(t *testing.T) {
 	data := make([]byte, 5)
 	n, err := f.Read(data)
 	require.NoError(err)
-	require.Equal(n, 5)
+	require.Equal(5, n)
 	require.Equal(string(data), testData)
 
 	files, err := c.ReadDir("/")
diff --git a/src/host/vfs/os.go b/src/host/vfs/os.go
index 035b66d..4d09b05 100644
--- a/src/host/vfs/os.go
+++ b/src/host/vfs/os.go
@@ -11,10 +11,19 @@ type OsFS struct {
 	hostDir string
 }
 
+// Stat implements Filesystem.
+func (fs *OsFS) Stat(filename string) (fs.FileInfo, error) {
+	if path.Clean(filename) == Separator {
+		return newDirInfo(Separator), nil
+	}
+
+	return os.Stat(path.Join(fs.hostDir, filename))
+}
+
 // Open implements Filesystem.
 func (fs *OsFS) Open(filename string) (File, error) {
 	if path.Clean(filename) == Separator {
-		return &Dir{}, nil
+		return NewDir(filename), nil
 	}
 
 	osfile, err := os.Open(path.Join(fs.hostDir, filename))
@@ -25,21 +34,9 @@ func (fs *OsFS) Open(filename string) (File, error) {
 }
 
 // ReadDir implements Filesystem.
-func (o *OsFS) ReadDir(dir string) (map[string]File, error) {
+func (o *OsFS) ReadDir(dir string) ([]fs.DirEntry, error) {
 	dir = path.Join(o.hostDir, dir)
-	entries, err := os.ReadDir(dir)
-	if err != nil {
-		return nil, err
-	}
-	out := map[string]File{}
-	for _, e := range entries {
-		if e.IsDir() {
-			out[e.Name()] = &Dir{}
-		} else {
-			out[e.Name()] = NewLazyOsFile(path.Join(dir, e.Name()))
-		}
-	}
-	return out, nil
+	return os.ReadDir(dir)
 }
 
 func NewOsFs(osDir string) *OsFS {
@@ -60,6 +57,11 @@ func NewOsFile(f *os.File) *OsFile {
 
 var _ File = &OsFile{}
 
+// Info implements File.
+func (f *OsFile) Info() (fs.FileInfo, error) {
+	return f.f.Stat()
+}
+
 // Close implements File.
 func (f *OsFile) Close() error {
 	return f.f.Close()
@@ -101,6 +103,9 @@ type LazyOsFile struct {
 	m    sync.Mutex
 	path string
 	file *os.File
+
+	// cached field
+	info fs.FileInfo
 }
 
 func NewLazyOsFile(path string) *LazyOsFile {
@@ -127,25 +132,49 @@ func (f *LazyOsFile) open() error {
 
 // Close implements File.
 func (f *LazyOsFile) Close() error {
+	if f.file == nil {
+		return nil
+	}
 	return f.file.Close()
 }
 
 // Read implements File.
 func (f *LazyOsFile) Read(p []byte) (n int, err error) {
+	err = f.open()
+	if err != nil {
+		return 0, err
+	}
 	return f.file.Read(p)
 }
 
 // ReadAt implements File.
 func (f *LazyOsFile) ReadAt(p []byte, off int64) (n int, err error) {
+	err = f.open()
+	if err != nil {
+		return 0, err
+	}
 	return f.file.ReadAt(p, off)
 }
 
 func (f *LazyOsFile) Stat() (fs.FileInfo, error) {
-	if f.file == nil {
-		return os.Stat(f.path)
-	} else {
-		return f.file.Stat()
+	f.m.Lock()
+	if f.info == nil {
+		if f.file == nil {
+			info, err := os.Stat(f.path)
+			if err != nil {
+				return nil, err
+			}
+			f.info = info
+		} else {
+			info, err := f.file.Stat()
+			if err != nil {
+				return nil, err
+			}
+			f.info = info
+		}
 	}
+	f.m.Unlock()
+	return f.info, nil
 }
 
 // Size implements File.
diff --git a/src/host/vfs/resolver.go b/src/host/vfs/resolver.go
index df1ffd8..9bab3b8 100644
--- a/src/host/vfs/resolver.go
+++ b/src/host/vfs/resolver.go
@@ -3,27 +3,27 @@ package vfs
 import (
 	"fmt"
 	"io/fs"
+	"path"
+	"slices"
 	"strings"
 	"sync"
 )
 
 type ResolveFS struct {
-	osDir    string
-	osFS     *OsFS
+	rootFS   Filesystem
 	resolver *resolver
 }
 
-func NewResolveFS(osDir string, factories map[string]FsFactory) *ResolveFS {
+func NewResolveFS(rootFs Filesystem, factories map[string]FsFactory) *ResolveFS {
 	return &ResolveFS{
-		osDir:    osDir,
-		osFS:     NewOsFs(osDir),
+		rootFS:   rootFs,
 		resolver: newResolver(factories),
 	}
 }
 
 // Open implements Filesystem.
 func (r *ResolveFS) Open(filename string) (File, error) {
-	fsPath, nestedFs, nestedFsPath, err := r.resolver.resolvePath(filename, r.osFS.Open)
+	fsPath, nestedFs, nestedFsPath, err := r.resolver.resolvePath(filename, r.rootFS.Open)
 	if err != nil {
 		return nil, err
 	}
@@ -31,12 +31,12 @@ func (r *ResolveFS) Open(filename string) (File, error) {
 		return nestedFs.Open(nestedFsPath)
 	}
 
-	return r.osFS.Open(fsPath)
+	return r.rootFS.Open(fsPath)
 }
 
 // ReadDir implements Filesystem.
-func (r *ResolveFS) ReadDir(dir string) (map[string]File, error) {
-	fsPath, nestedFs, nestedFsPath, err := r.resolver.resolvePath(dir, r.osFS.Open)
+func (r *ResolveFS) ReadDir(dir string) ([]fs.DirEntry, error) {
+	fsPath, nestedFs, nestedFsPath, err := r.resolver.resolvePath(dir, r.rootFS.Open)
 	if err != nil {
 		return nil, err
 	}
@@ -44,7 +44,32 @@ func (r *ResolveFS) ReadDir(dir string) (map[string]File, error) {
 		return nestedFs.ReadDir(nestedFsPath)
 	}
 
-	return r.osFS.ReadDir(fsPath)
+	entries, err := r.rootFS.ReadDir(fsPath)
+	if err != nil {
+		return nil, err
+	}
+	out := make([]fs.DirEntry, 0, len(entries))
+	for _, e := range entries {
+		if r.resolver.isNestedFs(e.Name()) {
+			out = append(out, newDirInfo(e.Name()))
+		} else {
+			out = append(out, e)
+		}
+	}
+	return out, nil
+}
+
+// Stat implements Filesystem.
+func (r *ResolveFS) Stat(filename string) (fs.FileInfo, error) {
+	fsPath, nestedFs, nestedFsPath, err := r.resolver.resolvePath(filename, r.rootFS.Open)
+	if err != nil {
+		return nil, err
+	}
+	if nestedFs != nil {
+		return nestedFs.Stat(nestedFsPath)
+	}
+
+	return r.rootFS.Stat(fsPath)
 }
 
 var _ Filesystem = &ResolveFS{}
@@ -69,8 +94,18 @@ type resolver struct {
 
 type openFile func(path string) (File, error)
 
+func (r *resolver) isNestedFs(f string) bool {
+	for ext := range r.factories {
+		if strings.HasSuffix(f, ext) {
+			return true
+		}
+	}
+	return true
+}
+
 // open requeue raw open, without resolver call
 func (r *resolver) resolvePath(name string, rawOpen openFile) (fsPath string, nestedFs Filesystem, nestedFsPath string, err error) {
+	name = path.Clean(name)
 	name = strings.TrimPrefix(name, Separator)
 	parts := strings.Split(name, Separator)
 
@@ -89,11 +124,12 @@ PARTS_LOOP:
 	}
 
 	if nestOn == -1 {
-		return clean(name), nil, "", nil
+		return AbsPath(name), nil, "", nil
 	}
 
-	fsPath = clean(strings.Join(parts[:nestOn], Separator))
-	nestedFsPath = clean(strings.Join(parts[nestOn:], Separator))
+	fsPath = AbsPath(path.Join(parts[:nestOn]...))
+
+	nestedFsPath = AbsPath(path.Join(parts[nestOn:]...))
 
 	// we dont need lock until now
 	// it must be before fsmap read to exclude race condition:
@@ -123,9 +159,8 @@ PARTS_LOOP:
 var ErrNotExist = fs.ErrNotExist
 
 func getFile[F File](m map[string]F, name string) (File, error) {
-	name = clean(name)
 	if name == Separator {
-		return &Dir{}, nil
+		return &dir{}, nil
 	}
 
 	f, ok := m[name]
@@ -135,27 +170,30 @@ func getFile[F File](m map[string]F, name string) (File, error) {
 
 	for p := range m {
 		if strings.HasPrefix(p, name) {
-			return &Dir{}, nil
+			return &dir{}, nil
 		}
 	}
 
 	return nil, ErrNotExist
 }
 
-func listFilesInDir[F File](m map[string]F, name string) (map[string]File, error) {
-	name = clean(name)
-
-	out := map[string]File{}
+func listDirFromFiles[F File](m map[string]F, name string) ([]fs.DirEntry, error) {
+	out := make([]fs.DirEntry, 0, len(m))
+	name = AddTrailSlash(name)
 	for p, f := range m {
 		if strings.HasPrefix(p, name) {
 			parts := strings.Split(trimRelPath(p, name), Separator)
 			if len(parts) == 1 {
-				out[parts[0]] = f
+				out = append(out, newFileInfo(parts[0], f.Size()))
 			} else {
-				out[parts[0]] = &Dir{}
+				out = append(out, newDirInfo(parts[0]))
 			}
+
 		}
 	}
+	out = slices.CompactFunc(out, func(de1, de2 fs.DirEntry) bool {
+		return de1.Name() == de2.Name()
+	})
 
 	return out, nil
 }
diff --git a/src/host/vfs/resolver_test.go b/src/host/vfs/resolver_test.go
index 401459f..ce2af5b 100644
--- a/src/host/vfs/resolver_test.go
+++ b/src/host/vfs/resolver_test.go
@@ -1,13 +1,21 @@
 package vfs
 
 import (
+	"io/fs"
 	"os"
+	"path"
 	"testing"
 
 	"github.com/stretchr/testify/require"
 )
 
 type Dummy struct {
+	name string
+}
+
+// Stat implements File.
+func (d *Dummy) Stat() (fs.FileInfo, error) {
+	return newFileInfo(d.name, 0), nil
 }
 
 func (d *Dummy) Size() int64 {
@@ -35,15 +43,20 @@ var _ File = &Dummy{}
 type DummyFs struct {
 }
 
+// Stat implements Filesystem.
+func (*DummyFs) Stat(filename string) (fs.FileInfo, error) {
+	return newFileInfo(path.Base(filename), 0), nil // TODO
+}
+
 func (d *DummyFs) Open(filename string) (File, error) {
 	return &Dummy{}, nil
 }
 
-func (d *DummyFs) ReadDir(path string) (map[string]File, error) {
+func (d *DummyFs) ReadDir(path string) ([]fs.DirEntry, error) {
 	if path == "/dir/here" {
-		return map[string]File{
-			"file1.txt": &Dummy{},
-			"file2.txt": &Dummy{},
+		return []fs.DirEntry{
+			newFileInfo("file1.txt", 0),
+			newFileInfo("file2.txt", 0),
 		}, nil
 	}
 
@@ -63,7 +76,7 @@ func TestResolver(t *testing.T) {
 			require.Equal("/f1.rar", path)
 			return &Dummy{}, nil
 		})
-		require.Nil(err)
+		require.NoError(err)
 		require.Equal("/f1.rar", fsPath)
 		require.Equal("/f2.rar", nestedFsPath)
 		require.IsType(&archive{}, nestedFs)
@@ -76,7 +89,7 @@ func TestResolver(t *testing.T) {
 			require.Equal("/", path)
 			return &Dummy{}, nil
 		})
-		require.Nil(err)
+		require.NoError(err)
 		require.Nil(nestedFs)
 		require.Equal("/", fsPath)
 		require.Equal("", nestedFsPath)
@@ -90,7 +103,7 @@ func TestResolver(t *testing.T) {
 			require.Equal("/", path)
 			return &Dummy{}, nil
 		})
-		require.Nil(err)
+		require.NoError(err)
 		require.Nil(nestedFs)
 		require.Equal("/", fsPath)
 		require.Equal("", nestedFsPath)
@@ -103,7 +116,7 @@ func TestResolver(t *testing.T) {
 			require.Equal("/f1.rar", path)
 			return &Dummy{}, nil
 		})
-		require.Nil(err)
+		require.NoError(err)
 		require.Equal("/f1.rar", fsPath)
 		require.Equal("/", nestedFsPath)
 		require.IsType(&archive{}, nestedFs)
@@ -116,7 +129,7 @@ func TestResolver(t *testing.T) {
 			require.Equal("/test1/f1.rar", path)
 			return &Dummy{}, nil
 		})
-		require.Nil(err)
+		require.NoError(err)
 		require.IsType(&archive{}, nestedFs)
 		require.Equal("/test1/f1.rar", fsPath)
 		require.Equal("/", nestedFsPath)
@@ -150,23 +163,43 @@ func TestFiles(t *testing.T) {
 	require := require.New(t)
 
 	files := map[string]*Dummy{
-		"/test/file.txt": &Dummy{},
+		"/test/file.txt":  &Dummy{},
+		"/test/file2.txt": &Dummy{},
+		"/test1/file.txt": &Dummy{},
 	}
 	{
 		file, err := getFile(files, "/test")
-		require.Nil(err)
-		require.Equal(&Dir{}, file)
+		require.NoError(err)
+		require.Equal(&dir{}, file)
 	}
 	{
 		file, err := getFile(files, "/test/file.txt")
-		require.Nil(err)
+		require.NoError(err)
 		require.Equal(&Dummy{}, file)
 	}
-
 	{
-		out, err := listFilesInDir(files, "/test")
-		require.Nil(err)
-		require.Contains(out, "file.txt")
-		require.Equal(&Dummy{}, out["file.txt"])
+		out, err := listDirFromFiles(files, "/test")
+		require.NoError(err)
+		require.Len(out, 2)
+		require.Equal("file.txt", out[0].Name())
+		require.Equal("file2.txt", out[1].Name())
+		require.False(out[0].IsDir())
+		require.False(out[1].IsDir())
+	}
+	{
+		out, err := listDirFromFiles(files, "/test1")
+		require.NoError(err)
+		require.Len(out, 1)
+		require.Equal("file.txt", out[0].Name())
+		require.False(out[0].IsDir())
+	}
+	{
+		out, err := listDirFromFiles(files, "/")
+		require.NoError(err)
+		require.Len(out, 2)
+		require.Equal("test", out[0].Name())
+		require.Equal("test1", out[1].Name())
+		require.True(out[0].IsDir())
+		require.True(out[1].IsDir())
 	}
 }
diff --git a/src/host/vfs/torrent.go b/src/host/vfs/torrent.go
index 2089f03..e4d9247 100644
--- a/src/host/vfs/torrent.go
+++ b/src/host/vfs/torrent.go
@@ -3,6 +3,8 @@ package vfs
 import (
 	"context"
 	"io"
+	"io/fs"
+	"path"
 	"sync"
 	"time"
 
@@ -14,10 +16,14 @@ import (
 var _ Filesystem = &TorrentFs{}
 
 type TorrentFs struct {
-	mu          sync.RWMutex
-	t           *torrent.Torrent
+	mu sync.Mutex
+	t  *torrent.Torrent
+
 	readTimeout int
 
+	//cache
+	filesCache map[string]*torrentFile
+
 	resolver *resolver
 }
 
@@ -30,22 +36,24 @@ func NewTorrentFs(t *torrent.Torrent, readTimeout int) *TorrentFs {
 }
 
 func (fs *TorrentFs) files() map[string]*torrentFile {
-	files := make(map[string]*torrentFile)
-	<-fs.t.GotInfo()
-	for _, file := range fs.t.Files() {
-		if file.Priority() == torrent.PiecePriorityNone {
-			continue
-		}
-
-		p := clean(file.Path())
-		files[p] = &torrentFile{
-			readerFunc: file.NewReader,
-			len:        file.Length(),
-			timeout:    fs.readTimeout,
+	if fs.filesCache == nil {
+		fs.mu.Lock()
+		<-fs.t.GotInfo()
+		files := fs.t.Files()
+		fs.filesCache = make(map[string]*torrentFile)
+		for _, file := range files {
+			p := AbsPath(file.Path())
+			fs.filesCache[p] = &torrentFile{
+				name:       path.Base(p),
+				readerFunc: file.NewReader,
+				len:        file.Length(),
+				timeout:    fs.readTimeout,
+			}
 		}
+		fs.mu.Unlock()
 	}
 
-	return files
+	return fs.filesCache
 }
 
 func (fs *TorrentFs) rawOpen(path string) (File, error) {
@@ -53,6 +61,36 @@ func (fs *TorrentFs) rawOpen(path string) (File, error) {
 	return file, err
 }
 
+func (fs *TorrentFs) rawStat(filename string) (fs.FileInfo, error) {
+	file, err := getFile(fs.files(), filename)
+	if err != nil {
+		return nil, err
+	}
+	if file.IsDir() {
+		return newDirInfo(path.Base(filename)), nil
+	} else {
+		return newFileInfo(path.Base(filename), file.Size()), nil
+	}
+
+}
+
+// Stat implements Filesystem.
+func (fs *TorrentFs) Stat(filename string) (fs.FileInfo, error) {
+	if filename == Separator {
+		return newDirInfo(filename), nil
+	}
+
+	fsPath, nestedFs, nestedFsPath, err := fs.resolver.resolvePath(filename, fs.rawOpen)
+	if err != nil {
+		return nil, err
+	}
+	if nestedFs != nil {
+		return nestedFs.Stat(nestedFsPath)
+	}
+
+	return fs.rawStat(fsPath)
+}
+
 func (fs *TorrentFs) Open(filename string) (File, error) {
 	fsPath, nestedFs, nestedFsPath, err := fs.resolver.resolvePath(filename, fs.rawOpen)
 	if err != nil {
@@ -65,7 +103,7 @@ func (fs *TorrentFs) Open(filename string) (File, error) {
 	return fs.rawOpen(fsPath)
 }
 
-func (fs *TorrentFs) ReadDir(name string) (map[string]File, error) {
+func (fs *TorrentFs) ReadDir(name string) ([]fs.DirEntry, error) {
 	fsPath, nestedFs, nestedFsPath, err := fs.resolver.resolvePath(name, fs.rawOpen)
 	if err != nil {
 		return nil, err
@@ -74,7 +112,7 @@ func (fs *TorrentFs) ReadDir(name string) (map[string]File, error) {
 		return nestedFs.ReadDir(nestedFsPath)
 	}
 
-	return listFilesInDir(fs.files(), fsPath)
+	return listDirFromFiles(fs.files(), fsPath)
 }
 
 func (fs *TorrentFs) Unlink(name string) error {
@@ -144,12 +182,18 @@ func (rw *readAtWrapper) Close() error {
 var _ File = &torrentFile{}
 
 type torrentFile struct {
+	name string
+
 	readerFunc func() torrent.Reader
 	reader     reader
 	len        int64
 	timeout    int
 }
 
+func (d *torrentFile) Stat() (fs.FileInfo, error) {
+	return newFileInfo(d.name, d.len), nil
+}
+
 func (d *torrentFile) load() {
 	if d.reader != nil {
 		return
diff --git a/src/host/vfs/utils.go b/src/host/vfs/utils.go
index 7f36933..e89977c 100644
--- a/src/host/vfs/utils.go
+++ b/src/host/vfs/utils.go
@@ -1,14 +1,25 @@
 package vfs
 
-import (
-	"path"
-	"strings"
-)
+import "strings"
 
 func trimRelPath(p, t string) string {
 	return strings.Trim(strings.TrimPrefix(p, t), "/")
 }
 
-func clean(p string) string {
-	return path.Clean(Separator + strings.ReplaceAll(p, "\\", "/"))
+// func clean(p string) string {
+// 	return path.Clean(Separator + strings.ReplaceAll(p, "\\", "/"))
+// }
+
+func AbsPath(p string) string {
+	if p == "" || p[0] != '/' {
+		return Separator + p
+	}
+	return p
+}
+
+func AddTrailSlash(p string) string {
+	if p == "" || p[len(p)-1] != '/' {
+		return p + Separator
+	}
+	return p
 }
diff --git a/src/http/http.go b/src/http/http.go
index 7f2fa27..55a6590 100644
--- a/src/http/http.go
+++ b/src/http/http.go
@@ -8,6 +8,7 @@ import (
 	"git.kmsign.ru/royalcat/tstor/src/config"
 	"git.kmsign.ru/royalcat/tstor/src/host/torrent"
 	"github.com/anacrolix/missinggo/v2/filecache"
+	"github.com/gin-contrib/pprof"
 	"github.com/gin-gonic/gin"
 	"github.com/rs/zerolog/log"
 	"github.com/shurcooL/httpfs/html/vfstemplate"
@@ -19,6 +20,7 @@ func New(fc *filecache.Cache, ss *torrent.Stats, s *torrent.Service, logPath str
 	r.Use(gin.Recovery())
 	r.Use(gin.ErrorLogger())
 	r.Use(Logger())
+	pprof.Register(r)
 
 	r.GET("/assets/*filepath", func(c *gin.Context) {
 		c.FileFromFS(c.Request.URL.Path, http.FS(tstor.Assets))
diff --git a/src/iio/wrapper_test.go b/src/iio/wrapper_test.go
index 071ce92..7d8a82d 100644
--- a/src/iio/wrapper_test.go
+++ b/src/iio/wrapper_test.go
@@ -16,7 +16,7 @@ func TestSeekerWrapper(t *testing.T) {
 
 	require := require.New(t)
 
-	mf := vfs.NewMemoryFile(testData)
+	mf := vfs.NewMemoryFile("text.txt", testData)
 
 	r := iio.NewSeekerWrapper(mf, mf.Size())
 	defer r.Close()
diff --git a/src/mounts/fuse/mount.go b/src/mounts/fuse/mount.go
index da6a390..a598612 100644
--- a/src/mounts/fuse/mount.go
+++ b/src/mounts/fuse/mount.go
@@ -174,13 +174,13 @@ func (fh *fileHandler) ListDir(path string) ([]string, error) {
 	fh.mu.RLock()
 	defer fh.mu.RUnlock()
 
-	var out []string
 	files, err := fh.fs.ReadDir(path)
 	if err != nil {
 		return nil, err
 	}
-	for p := range files {
-		out = append(out, p)
+	out := make([]string, 0, len(files))
+	for _, p := range files {
+		out = append(out, p.Name())
 	}
 
 	return out, nil
diff --git a/src/mounts/fuse/mount_test.go b/src/mounts/fuse/mount_test.go
index 3d16ca4..5a973af 100644
--- a/src/mounts/fuse/mount_test.go
+++ b/src/mounts/fuse/mount_test.go
@@ -25,7 +25,7 @@ func TestHandler(t *testing.T) {
 	h := NewHandler(false, p)
 
 	mem := vfs.NewMemoryFS(map[string]*vfs.MemoryFile{
-		"/test.txt": vfs.NewMemoryFile([]byte("test")),
+		"/test.txt": vfs.NewMemoryFile("test.txt", []byte("test")),
 	})
 
 	err := h.Mount(mem)
@@ -52,7 +52,7 @@ func TestHandlerDriveLetter(t *testing.T) {
 	h := NewHandler(false, p)
 
 	mem := vfs.NewMemoryFS(map[string]*vfs.MemoryFile{
-		"/test.txt": vfs.NewMemoryFile([]byte("test")),
+		"/test.txt": vfs.NewMemoryFile("test.txt", []byte("test")),
 	})
 
 	err := h.Mount(mem)
diff --git a/src/mounts/httpfs/httpfs.go b/src/mounts/httpfs/httpfs.go
index f593903..303cf2d 100644
--- a/src/mounts/httpfs/httpfs.go
+++ b/src/mounts/httpfs/httpfs.go
@@ -21,32 +21,37 @@ func NewHTTPFS(fs vfs.Filesystem) *HTTPFS {
 	return &HTTPFS{fs: fs}
 }
 
-func (fs *HTTPFS) Open(name string) (http.File, error) {
-	f, err := fs.fs.Open(name)
+func (hfs *HTTPFS) Open(name string) (http.File, error) {
+	f, err := hfs.fs.Open(name)
 	if err != nil {
 		return nil, err
 	}
 
-	fi := vfs.NewFileInfo(name, f.Size(), f.IsDir())
-
-	// TODO make this lazy
-	fis, err := fs.filesToFileInfo(name)
-	if err != nil {
-		return nil, err
+	var fis []fs.FileInfo
+	if f.IsDir() {
+		// TODO make this lazy
+		fis, err = hfs.filesToFileInfo(name)
+		if err != nil {
+			return nil, err
+		}
 	}
 
-	return newHTTPFile(f, fis, fi), nil
+	return newHTTPFile(f, fis), nil
 }
 
-func (fs *HTTPFS) filesToFileInfo(path string) ([]fs.FileInfo, error) {
-	files, err := fs.fs.ReadDir(path)
+func (hfs *HTTPFS) filesToFileInfo(name string) ([]fs.FileInfo, error) {
+	files, err := hfs.fs.ReadDir(name)
 	if err != nil {
 		return nil, err
 	}
 
-	var out []os.FileInfo
-	for n, f := range files {
-		out = append(out, vfs.NewFileInfo(n, f.Size(), f.IsDir()))
+	out := make([]os.FileInfo, 0, len(files))
+	for _, f := range files {
+		info, err := f.Info()
+		if err != nil {
+			return nil, err
+		}
+		out = append(out, info)
 	}
 
 	return out, nil
@@ -55,33 +60,32 @@ func (fs *HTTPFS) filesToFileInfo(path string) ([]fs.FileInfo, error) {
 var _ http.File = &httpFile{}
 
 type httpFile struct {
+	f vfs.File
+
 	iio.ReaderSeeker
 
 	mu sync.Mutex
 	// dirPos is protected by mu.
 	dirPos     int
 	dirContent []os.FileInfo
-
-	fi fs.FileInfo
 }
 
-func newHTTPFile(f vfs.File, fis []fs.FileInfo, fi fs.FileInfo) *httpFile {
+func newHTTPFile(f vfs.File, dirContent []os.FileInfo) *httpFile {
 	return &httpFile{
-		dirContent: fis,
-		fi:         fi,
-
+		f:            f,
+		dirContent:   dirContent,
 		ReaderSeeker: iio.NewSeekerWrapper(f, f.Size()),
 	}
 }
 
 func (f *httpFile) Readdir(count int) ([]fs.FileInfo, error) {
-	f.mu.Lock()
-	defer f.mu.Unlock()
-
-	if !f.fi.IsDir() {
+	if !f.f.IsDir() {
 		return nil, os.ErrInvalid
 	}
 
+	f.mu.Lock()
+	defer f.mu.Unlock()
+
 	old := f.dirPos
 	if old >= len(f.dirContent) {
 		// The os.File Readdir docs say that at the end of a directory,
@@ -105,5 +109,5 @@ func (f *httpFile) Readdir(count int) ([]fs.FileInfo, error) {
 }
 
 func (f *httpFile) Stat() (fs.FileInfo, error) {
-	return f.fi, nil
+	return f.f.Stat()
 }
diff --git a/src/mounts/nfs/handler.go b/src/mounts/nfs/handler.go
new file mode 100644
index 0000000..d340062
--- /dev/null
+++ b/src/mounts/nfs/handler.go
@@ -0,0 +1,16 @@
+package nfs
+
+import (
+	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
+	nfs "github.com/willscott/go-nfs"
+	nfshelper "github.com/willscott/go-nfs/helpers"
+)
+
+func NewNFSv3Handler(fs vfs.Filesystem) (nfs.Handler, error) {
+	bfs := &billyFsWrapper{fs: fs}
+	handler := nfshelper.NewNullAuthHandler(bfs)
+	cacheHelper := nfshelper.NewCachingHandler(handler, 1024*16)
+	//  cacheHelper := NewCachingHandler(handler)
+
+	return cacheHelper, nil
+}
diff --git a/src/mounts/nfs/wrapper-v3.go b/src/mounts/nfs/wrapper-v3.go
new file mode 100644
index 0000000..195bd41
--- /dev/null
+++ b/src/mounts/nfs/wrapper-v3.go
@@ -0,0 +1,190 @@
+package nfs
+
+import (
+	"io/fs"
+	"path/filepath"
+
+	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
+	"github.com/go-git/go-billy/v5"
+)
+
+type billyFsWrapper struct {
+	fs vfs.Filesystem
+}
+
+var _ billy.Filesystem = (*billyFsWrapper)(nil)
+var _ billy.Dir = (*billyFsWrapper)(nil)
+
+// Chroot implements billy.Filesystem.
+func (*billyFsWrapper) Chroot(path string) (billy.Filesystem, error) {
+	return nil, billy.ErrNotSupported
+}
+
+// Create implements billy.Filesystem.
+func (*billyFsWrapper) Create(filename string) (billy.File, error) {
+	return nil, billy.ErrNotSupported
+}
+
+// Join implements billy.Filesystem.
+func (*billyFsWrapper) Join(elem ...string) string {
+	return filepath.Join(elem...)
+}
+
+// Lstat implements billy.Filesystem.
+func (fs *billyFsWrapper) Lstat(filename string) (fs.FileInfo, error) {
+	info, err := fs.fs.Stat(filename)
+	if err != nil {
+		return nil, billyErr(err)
+	}
+	return info, nil
+}
+
+// MkdirAll implements billy.Filesystem.
+func (*billyFsWrapper) MkdirAll(filename string, perm fs.FileMode) error {
+	return billy.ErrNotSupported
+}
+
+// Open implements billy.Filesystem.
+func (f *billyFsWrapper) Open(filename string) (billy.File, error) {
+	file, err := f.fs.Open(filename)
+	if err != nil {
+		return nil, billyErr(err)
+	}
+	return &billyFile{
+		name: filename,
+		file: file,
+	}, nil
+}
+
+// OpenFile implements billy.Filesystem.
+func (f *billyFsWrapper) OpenFile(filename string, flag int, perm fs.FileMode) (billy.File, error) {
+	file, err := f.fs.Open(filename)
+	if err != nil {
+		return nil, billyErr(err)
+	}
+	return &billyFile{
+		name: filename,
+		file: file,
+	}, nil
+}
+
+// ReadDir implements billy.Filesystem.
+func (bfs *billyFsWrapper) ReadDir(path string) ([]fs.FileInfo, error) {
+	ffs, err := bfs.fs.ReadDir(path)
+	if err != nil {
+		return nil, billyErr(err)
+	}
+
+	out := make([]fs.FileInfo, 0, len(ffs))
+	for _, v := range ffs {
+		if info, ok := v.(fs.FileInfo); ok {
+			out = append(out, info)
+		} else {
+			info, err := v.Info()
+			if err != nil {
+				return nil, err
+			}
+			out = append(out, info)
+		}
+
+	}
+	return out, nil
+}
+
+// Readlink implements billy.Filesystem.
+func (*billyFsWrapper) Readlink(link string) (string, error) {
+	return "", billy.ErrNotSupported
+}
+
+// Remove implements billy.Filesystem.
+func (*billyFsWrapper) Remove(filename string) error {
+	return billy.ErrNotSupported
+}
+
+// Rename implements billy.Filesystem.
+func (*billyFsWrapper) Rename(oldpath string, newpath string) error {
+	return billy.ErrNotSupported
+}
+
+// Root implements billy.Filesystem.
+func (*billyFsWrapper) Root() string {
+	return "/"
+}
+
+// Stat implements billy.Filesystem.
+func (f *billyFsWrapper) Stat(filename string) (fs.FileInfo, error) {
+	info, err := f.fs.Stat(filename)
+	if err != nil {
+		return nil, billyErr(err)
+	}
+	return info, nil
+}
+
+// Symlink implements billy.Filesystem.
+func (*billyFsWrapper) Symlink(target string, link string) error {
+	return billyErr(vfs.ErrNotImplemented)
+}
+
+// TempFile implements billy.Filesystem.
+func (*billyFsWrapper) TempFile(dir string, prefix string) (billy.File, error) {
+	return nil, billyErr(vfs.ErrNotImplemented)
+}
+
+type billyFile struct {
+	name string
+	file vfs.File
+}
+
+var _ billy.File = (*billyFile)(nil)
+
+// Close implements billy.File.
+func (f *billyFile) Close() error {
+	return f.Close()
+}
+
+// Name implements billy.File.
+func (f *billyFile) Name() string {
+	return f.name
+}
+
+// Read implements billy.File.
+func (f *billyFile) Read(p []byte) (n int, err error) {
+	return f.Read(p)
+}
+
+// ReadAt implements billy.File.
+func (f *billyFile) ReadAt(p []byte, off int64) (n int, err error) {
+	return f.ReadAt(p, off)
+}
+
+// Seek implements billy.File.
+func (*billyFile) Seek(offset int64, whence int) (int64, error) {
+	return 0, billyErr(vfs.ErrNotImplemented)
+}
+
+// Truncate implements billy.File.
+func (*billyFile) Truncate(size int64) error {
+	return billyErr(vfs.ErrNotImplemented)
+}
+
+// Write implements billy.File.
+func (*billyFile) Write(p []byte) (n int, err error) {
+	return 0, billyErr(vfs.ErrNotImplemented)
+}
+
+// Lock implements billy.File.
+func (*billyFile) Lock() error {
+	return nil // TODO
+}
+
+// Unlock implements billy.File.
+func (*billyFile) Unlock() error {
+	return nil // TODO
+}
+
+func billyErr(err error) error {
+	if err == vfs.ErrNotImplemented {
+		return billy.ErrNotSupported
+	}
+	return err
+}
diff --git a/src/mounts/nfs/wrapper-v4.go b/src/mounts/nfs/wrapper-v4.go
new file mode 100644
index 0000000..e1e7bf3
--- /dev/null
+++ b/src/mounts/nfs/wrapper-v4.go
@@ -0,0 +1,164 @@
+package nfs
+
+// import (
+// 	"io/fs"
+
+// 	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
+// 	nfsfs "github.com/smallfz/libnfs-go/fs"
+// )
+
+// type nfsFsWrapper struct {
+// 	fs vfs.Filesystem
+// }
+
+// var _ nfsfs.FS = (*nfsFsWrapper)(nil)
+
+// // Attributes implements fs.FS.
+// func (*nfsFsWrapper) Attributes() *nfsfs.Attributes {
+// 	return &nfsfs.Attributes{
+// 		LinkSupport:     true,
+// 		SymlinkSupport:  false, // unsopported
+// 		ChownRestricted: true,  // unsopported
+// 		MaxName:         255,   // common value
+// 		NoTrunc:         false,
+// 	}
+// }
+
+// // Stat implements fs.FS.
+// func (*nfsFsWrapper) Stat(string) (nfsfs.FileInfo, error) {
+// 	panic("unimplemented")
+// }
+
+// // Chmod implements fs.FS.
+// func (*nfsFsWrapper) Chmod(string, fs.FileMode) error {
+// 	panic("unimplemented")
+// }
+
+// // Chown implements fs.FS.
+// func (*nfsFsWrapper) Chown(string, int, int) error {
+// 	panic("unimplemented")
+// }
+
+// // GetFileId implements fs.FS.
+// func (*nfsFsWrapper) GetFileId(nfsfs.FileInfo) uint64 {
+// 	panic("unimplemented")
+// }
+
+// // GetHandle implements fs.FS.
+// func (*nfsFsWrapper) GetHandle(nfsfs.FileInfo) ([]byte, error) {
+// 	panic("unimplemented")
+// }
+
+// // GetRootHandle implements fs.FS.
+// func (*nfsFsWrapper) GetRootHandle() []byte {
+// 	panic("unimplemented")
+// }
+
+// // Link implements fs.FS.
+// func (*nfsFsWrapper) Link(string, string) error {
+// 	panic("unimplemented")
+// }
+
+// // MkdirAll implements fs.FS.
+// func (*nfsFsWrapper) MkdirAll(string, fs.FileMode) error {
+// 	panic("unimplemented")
+// }
+
+// // Open implements fs.FS.
+// func (w *nfsFsWrapper) Open(name string) (nfsfs.File, error) {
+// 	f, err := w.fs.Open(name)
+// 	if err != nil {
+// 		return nil, nfsErr(err)
+// 	}
+// }
+
+// // OpenFile implements fs.FS.
+// func (w *nfsFsWrapper) OpenFile(string, int, fs.FileMode) (nfsfs.File, error) {
+// 	panic("unimplemented")
+// }
+
+// // Readlink implements fs.FS.
+// func (*nfsFsWrapper) Readlink(string) (string, error) {
+// 	panic("unimplemented")
+// }
+
+// // Remove implements fs.FS.
+// func (*nfsFsWrapper) Remove(string) error {
+// 	panic("unimplemented")
+// }
+
+// // Rename implements fs.FS.
+// func (*nfsFsWrapper) Rename(string, string) error {
+// 	panic("unimplemented")
+// }
+
+// // ResolveHandle implements fs.FS.
+// func (*nfsFsWrapper) ResolveHandle([]byte) (string, error) {
+// 	panic("unimplemented")
+// }
+
+// // Symlink implements fs.FS.
+// func (*nfsFsWrapper) Symlink(string, string) error {
+// 	return NotImplementedError
+// }
+
+// var NotImplementedError = vfs.NotImplemented
+
+// func nfsErr(err error) error {
+// 	if err == vfs.NotImplemented {
+// 		return NotImplementedError
+// 	}
+// 	return err
+// }
+
+// type nfsFile struct {
+// 	name string
+// 	f    vfs.File
+// }
+
+// // Close implements fs.File.
+// func (f *nfsFile) Close() error {
+// 	return f.f.Close()
+// }
+
+// // Name implements fs.File.
+// func (f *nfsFile) Name() string {
+// 	return f.name
+// }
+
+// // Read implements fs.File.
+// func (f *nfsFile) Read(p []byte) (n int, err error) {
+// 	return f.f.Read(p)
+// }
+
+// // Readdir implements fs.File.
+// func (f *nfsFile) Readdir(int) ([]nfsfs.FileInfo, error) {
+// 	f.f.IsDir()
+// }
+
+// // Seek implements fs.File.
+// func (*nfsFile) Seek(offset int64, whence int) (int64, error) {
+// 	panic("unimplemented")
+// }
+
+// // Stat implements fs.File.
+// func (*nfsFile) Stat() (nfsfs.FileInfo, error) {
+// 	panic("unimplemented")
+// }
+
+// // Sync implements fs.File.
+// func (*nfsFile) Sync() error {
+// 	panic("unimplemented")
+// }
+
+// // Truncate implements fs.File.
+// func (*nfsFile) Truncate() error {
+// 	panic("unimplemented")
+// }
+
+// // Write implements fs.File.
+// func (*nfsFile) Write(p []byte) (n int, err error) {
+// 	panic("unimplemented")
+// }
+
+// var _ nfsfs.File = (*nfsFile)(nil)
diff --git a/src/mounts/webdav/fs.go b/src/mounts/webdav/fs.go
index 29f2727..9ff2561 100644
--- a/src/mounts/webdav/fs.go
+++ b/src/mounts/webdav/fs.go
@@ -3,8 +3,9 @@ package webdav
 import (
 	"context"
 	"io"
+	"io/fs"
 	"os"
-	"path/filepath"
+	"path"
 	"sync"
 	"time"
 
@@ -24,30 +25,25 @@ func newFS(fs vfs.Filesystem) *WebDAV {
 }
 
 func (wd *WebDAV) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
-	p := "/" + name
+	name = vfs.AbsPath(name)
+
 	// TODO handle flag and permissions
-	f, err := wd.lookupFile(p)
+	f, err := wd.lookupFile(name)
 	if err != nil {
 		return nil, err
 	}
 
-	wdf := newFile(filepath.Base(p), f, func() ([]os.FileInfo, error) {
-		return wd.listDir(p)
+	wdf := newFile(path.Base(name), f, func() ([]fs.FileInfo, error) {
+		return wd.listDir(name)
 	})
 	return wdf, nil
 }
 
-func (wd *WebDAV) Stat(ctx context.Context, name string) (os.FileInfo, error) {
-	p := "/" + name
-	f, err := wd.lookupFile(p)
-	if err != nil {
-		return nil, err
-	}
-	fi := newFileInfo(name, f.Size(), f.IsDir())
-	return fi, nil
+func (wd *WebDAV) Stat(ctx context.Context, name string) (fs.FileInfo, error) {
+	return wd.fs.Stat(vfs.AbsPath(name))
 }
 
-func (wd *WebDAV) Mkdir(ctx context.Context, name string, perm os.FileMode) error {
+func (wd *WebDAV) Mkdir(ctx context.Context, name string, perm fs.FileMode) error {
 	return webdav.ErrNotImplemented
 }
 
@@ -59,8 +55,8 @@ func (wd *WebDAV) Rename(ctx context.Context, oldName, newName string) error {
 	return webdav.ErrNotImplemented
 }
 
-func (wd *WebDAV) lookupFile(path string) (vfs.File, error) {
-	return wd.fs.Open(path)
+func (wd *WebDAV) lookupFile(name string) (vfs.File, error) {
+	return wd.fs.Open(path.Clean(name))
 }
 
 func (wd *WebDAV) listDir(path string) ([]os.FileInfo, error) {
@@ -69,9 +65,13 @@ func (wd *WebDAV) listDir(path string) ([]os.FileInfo, error) {
 		return nil, err
 	}
 
-	var out []os.FileInfo
-	for n, f := range files {
-		out = append(out, newFileInfo(n, f.Size(), f.IsDir()))
+	out := make([]os.FileInfo, 0, len(files))
+	for _, f := range files {
+		info, err := f.Info()
+		if err != nil {
+			return nil, err
+		}
+		out = append(out, info)
 	}
 
 	return out, nil
diff --git a/src/mounts/webdav/fs_test.go b/src/mounts/webdav/fs_test.go
index 5d703e8..606696f 100644
--- a/src/mounts/webdav/fs_test.go
+++ b/src/mounts/webdav/fs_test.go
@@ -13,16 +13,17 @@ import (
 
 func TestWebDAVFilesystem(t *testing.T) {
 	t.Parallel()
+	ctx := context.Background()
 
 	require := require.New(t)
 
 	mfs := vfs.NewMemoryFS(map[string]*vfs.MemoryFile{
-		"/folder/file.txt": vfs.NewMemoryFile([]byte("test file content.")),
+		"/folder/file.txt": vfs.NewMemoryFile("file.txt", []byte("test file content.")),
 	})
 
 	wfs := newFS(mfs)
 
-	dir, err := wfs.OpenFile(context.Background(), "/", 0, 0)
+	dir, err := wfs.OpenFile(ctx, "/", 0, 0)
 	require.NoError(err)
 
 	fi, err := dir.Readdir(0)
@@ -30,7 +31,7 @@ func TestWebDAVFilesystem(t *testing.T) {
 	require.Len(fi, 1)
 	require.Equal("folder", fi[0].Name())
 
-	file, err := wfs.OpenFile(context.Background(), "/folder/file.txt", 0, 0)
+	file, err := wfs.OpenFile(ctx, "/folder/file.txt", 0, 0)
 	require.NoError(err)
 	_, err = file.Readdir(0)
 	require.ErrorIs(err, os.ErrInvalid)
@@ -57,7 +58,7 @@ func TestWebDAVFilesystem(t *testing.T) {
 	fInfo, err := wfs.Stat(context.Background(), "/folder/file.txt")
 	require.NoError(err)
 	require.Equal("/folder/file.txt", fInfo.Name())
-	require.Equal(false, fInfo.IsDir())
+	require.False(fInfo.IsDir())
 	require.Equal(int64(18), fInfo.Size())
 }
 
@@ -67,7 +68,7 @@ func TestErrNotImplemented(t *testing.T) {
 	require := require.New(t)
 
 	mfs := vfs.NewMemoryFS(map[string]*vfs.MemoryFile{
-		"/folder/file.txt": vfs.NewMemoryFile([]byte("test file content.")),
+		"/folder/file.txt": vfs.NewMemoryFile("file.txt", []byte("test file content.")),
 	})
 
 	wfs := newFS(mfs)
diff --git a/src/mounts/webdav/http.go b/src/mounts/webdav/http.go
index 3b95a83..80e4fdd 100644
--- a/src/mounts/webdav/http.go
+++ b/src/mounts/webdav/http.go
@@ -9,11 +9,12 @@ import (
 )
 
 func NewWebDAVServer(fs vfs.Filesystem, port int, user, pass string) error {
-	log.Info().Str("host", fmt.Sprintf("0.0.0.0:%d", port)).Msg("starting webDAV server")
 
 	srv := newHandler(fs)
 
-	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+	serveMux := http.NewServeMux()
+
+	serveMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
 		username, password, _ := r.BasicAuth()
 		if username == user && password == pass {
 			srv.ServeHTTP(w, r)
@@ -22,8 +23,16 @@ func NewWebDAVServer(fs vfs.Filesystem, port int, user, pass string) error {
 
 		w.Header().Set("WWW-Authenticate", `Basic realm="BASIC WebDAV REALM"`)
 		w.WriteHeader(401)
-		w.Write([]byte("401 Unauthorized\n"))
+		_, _ = w.Write([]byte("401 Unauthorized\n"))
 	})
 
-	return http.ListenAndServe(fmt.Sprintf("0.0.0.0:%d", port), nil)
+	//nolint:exhaustruct
+	httpServer := &http.Server{
+		Addr:    fmt.Sprintf("0.0.0.0:%d", port),
+		Handler: serveMux,
+	}
+
+	log.Info().Str("host", httpServer.Addr).Msg("starting webDAV server")
+
+	return httpServer.ListenAndServe()
 }

From 033220656097a1b809fc5ec03ec7a25028b2c9ca Mon Sep 17 00:00:00 2001
From: royalcat <k.adamovich20@gmail.com>
Date: Tue, 26 Dec 2023 01:11:03 +0300
Subject: [PATCH 04/18] [feature] file exclude

---
 cmd/tstor/main.go                 | 13 ++++-
 go.mod                            |  9 +++-
 go.sum                            | 44 +++++++++++++++
 src/host/repository/repository.go | 90 +++++++++++++++++++++++++++++++
 src/host/torrent/service.go       |  9 ++--
 src/host/vfs/archive.go           |  5 +-
 src/host/vfs/fs.go                |  1 +
 src/host/vfs/memory.go            |  5 ++
 src/host/vfs/os.go                |  5 ++
 src/host/vfs/resolver.go          | 13 +++++
 src/host/vfs/resolver_test.go     |  4 ++
 src/host/vfs/torrent.go           | 62 +++++++++++++++++----
 src/mounts/webdav/fs.go           |  2 +-
 13 files changed, 243 insertions(+), 19 deletions(-)
 create mode 100644 src/host/repository/repository.go

diff --git a/cmd/tstor/main.go b/cmd/tstor/main.go
index eddba22..8ded9b5 100644
--- a/cmd/tstor/main.go
+++ b/cmd/tstor/main.go
@@ -14,6 +14,7 @@ import (
 
 	"git.kmsign.ru/royalcat/tstor/src/config"
 	"git.kmsign.ru/royalcat/tstor/src/host"
+	"git.kmsign.ru/royalcat/tstor/src/host/repository"
 	"git.kmsign.ru/royalcat/tstor/src/host/torrent"
 	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
 	"github.com/rs/zerolog/log"
@@ -72,6 +73,14 @@ func run(configPath string) error {
 		log.Err(err).Msg("set priority failed")
 	}
 
+	if err := os.MkdirAll(filepath.Join(conf.TorrentClient.MetadataFolder, "meta"), 0744); err != nil {
+		return fmt.Errorf("error creating metadata folder: %w", err)
+	}
+	rep, err := repository.NewTorrentMetaRepository(filepath.Join(conf.TorrentClient.MetadataFolder, "meta"))
+	if err != nil {
+		return err
+	}
+
 	if err := os.MkdirAll(conf.TorrentClient.MetadataFolder, 0744); err != nil {
 		return fmt.Errorf("error creating metadata folder: %w", err)
 	}
@@ -100,7 +109,7 @@ func run(configPath string) error {
 	c.AddDhtNodes(conf.TorrentClient.DHTNodes)
 	defer c.Close()
 
-	ts := torrent.NewService(c, conf.TorrentClient.AddTimeout, conf.TorrentClient.ReadTimeout)
+	ts := torrent.NewService(c, rep, conf.TorrentClient.AddTimeout, conf.TorrentClient.ReadTimeout)
 
 	if err := os.MkdirAll(conf.DataFolder, 0744); err != nil {
 		return fmt.Errorf("error creating data folder: %w", err)
@@ -139,7 +148,7 @@ func run(configPath string) error {
 			// 	c.FileFromFS(path, httpfs)
 			// })
 
-			// log.Info().Str("host", fmt.Sprintf("0.0.0.0:%d", conf.Mounts.HttpFs.Port)).Msg("starting HTTPFS")
+			log.Info().Str("host", fmt.Sprintf("0.0.0.0:%d", conf.Mounts.HttpFs.Port)).Msg("starting HTTPFS")
 			// if err := r.Run(fmt.Sprintf("0.0.0.0:%d", conf.Mounts.HttpFs.Port)); err != nil {
 			// 	log.Error().Err(err).Msg("error starting HTTPFS")
 			// }
diff --git a/go.mod b/go.mod
index a9b95cd..5ad4fc0 100644
--- a/go.mod
+++ b/go.mod
@@ -22,16 +22,21 @@ require (
 	github.com/knadh/koanf/v2 v2.0.1
 	github.com/mattn/go-colorable v0.1.13
 	github.com/nwaples/rardecode/v2 v2.0.0-beta.2
+	github.com/philippgille/gokv v0.6.0
+	github.com/philippgille/gokv/badgerdb v0.6.0
+	github.com/philippgille/gokv/encoding v0.0.0-20191011213304-eb77f15b9c61
 	github.com/rs/zerolog v1.31.0
 	github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c
 	github.com/stretchr/testify v1.8.4
 	github.com/urfave/cli/v2 v2.26.0
 	github.com/willscott/go-nfs v0.0.1
+	golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611
 	golang.org/x/net v0.19.0
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1
 )
 
 require (
+	github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
 	github.com/RoaringBitmap/roaring v1.6.0 // indirect
 	github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect
 	github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
@@ -59,7 +64,9 @@ require (
 	github.com/chenzhuoyu/iasm v0.9.1 // indirect
 	github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/dgraph-io/badger v1.6.0 // indirect
 	github.com/dgraph-io/ristretto v0.1.1 // indirect
+	github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 // indirect
 	github.com/dustin/go-humanize v1.0.1 // indirect
 	github.com/fatih/structs v1.1.0 // indirect
 	github.com/fsnotify/fsnotify v1.7.0 // indirect
@@ -99,6 +106,7 @@ require (
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/mschoch/smat v0.2.0 // indirect
 	github.com/pelletier/go-toml/v2 v2.1.1 // indirect
+	github.com/philippgille/gokv/util v0.0.0-20191011213304-eb77f15b9c61 // indirect
 	github.com/pierrec/lz4/v4 v4.1.19 // indirect
 	github.com/pion/datachannel v1.5.5 // indirect
 	github.com/pion/dtls/v2 v2.2.8 // indirect
@@ -137,7 +145,6 @@ require (
 	go4.org v0.0.0-20230225012048-214862532bf5 // indirect
 	golang.org/x/arch v0.6.0 // indirect
 	golang.org/x/crypto v0.16.0 // indirect
-	golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 // indirect
 	golang.org/x/sync v0.5.0 // indirect
 	golang.org/x/sys v0.15.0 // indirect
 	golang.org/x/text v0.14.0 // indirect
diff --git a/go.sum b/go.sum
index b8ad50d..fb686fc 100644
--- a/go.sum
+++ b/go.sum
@@ -19,6 +19,9 @@ crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU=
 filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
+github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
+github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
+github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w=
@@ -98,6 +101,7 @@ github.com/anacrolix/utp v0.2.0/go.mod h1:HGk4GYQw1O/3T1+yhqT/F6EcBd+AAwlo9dYErN
 github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
 github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
 github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
 github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
 github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
 github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI=
@@ -141,13 +145,19 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
 github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
 github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgraph-io/badger v1.6.0 h1:DshxFxZWXUcO0xX476VJC07Xsr6ZCBVRHKZ93Oh7Evo=
+github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
 github.com/dgraph-io/badger/v4 v4.2.0 h1:kJrlajbXXL9DFTNuhhu9yCx7JJa4qpYWxtE8BzuWsEs=
 github.com/dgraph-io/badger/v4 v4.2.0/go.mod h1:qfCqhPoWDFJRx1gp5QwwyGo8xk1lbHUxvK9nK0OGAak=
 github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
@@ -224,6 +234,8 @@ github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqR
 github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
+github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
+github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
 github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
 github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
@@ -310,6 +322,7 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
 github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
 github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
@@ -320,6 +333,7 @@ github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq
 github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
 github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@@ -364,6 +378,7 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
 github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
 github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
+github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
 github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
@@ -374,6 +389,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
 github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
@@ -403,10 +420,23 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J
 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
 github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
 github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
 github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
 github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
 github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
+github.com/philippgille/gokv v0.0.0-20191001201555-5ac9a20de634/go.mod h1:OCoWPt+mbYuTO1FUVrQ2SxQU0oaaHBsn6lRhFX3JHOc=
+github.com/philippgille/gokv v0.5.1-0.20191011213304-eb77f15b9c61/go.mod h1:OCoWPt+mbYuTO1FUVrQ2SxQU0oaaHBsn6lRhFX3JHOc=
+github.com/philippgille/gokv v0.6.0 h1:fNEx/tSwV73nzlYd3iRYB8F+SEVJNNFzH1gsaT8SK2c=
+github.com/philippgille/gokv v0.6.0/go.mod h1:tjXRFw9xDHgxLS8WJdfYotKGWp8TWqu4RdXjMDG/XBo=
+github.com/philippgille/gokv/badgerdb v0.6.0 h1:4Qigf2SpyXLF8KaM5nA5/D/0aD/bZevuAnrW4ZsDsjA=
+github.com/philippgille/gokv/badgerdb v0.6.0/go.mod h1:3u2avs8gtmCc0R0Bw4jKV8aaDfLb5V9JToSASyhpFGM=
+github.com/philippgille/gokv/encoding v0.0.0-20191011213304-eb77f15b9c61 h1:IgQDuUPuEFVf22mBskeCLAtvd5c9XiiJG2UYud6eGHI=
+github.com/philippgille/gokv/encoding v0.0.0-20191011213304-eb77f15b9c61/go.mod h1:SjxSrCoeYrYn85oTtroyG1ePY8aE72nvLQlw8IYwAN8=
+github.com/philippgille/gokv/test v0.0.0-20191011213304-eb77f15b9c61 h1:4tVyBgfpK0NSqu7tNZTwYfC/pbyWUR2y+O7mxEg5BTQ=
+github.com/philippgille/gokv/test v0.0.0-20191011213304-eb77f15b9c61/go.mod h1:EUc+s9ONc1+VOr9NUEd8S0YbGRrQd/gz/p+2tvwt12s=
+github.com/philippgille/gokv/util v0.0.0-20191011213304-eb77f15b9c61 h1:ril/jI0JgXNjPWwDkvcRxlZ09kgHXV2349xChjbsQ4o=
+github.com/philippgille/gokv/util v0.0.0-20191011213304-eb77f15b9c61/go.mod h1:2dBhsJgY/yVIkjY5V3AnDUxUbEPzT6uQ3LvoVT8TR20=
 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
 github.com/pierrec/lz4/v4 v4.1.19 h1:tYLzDnjDXh9qIxSTKHwXwOYmm9d887Y7Y1ZkyXYHAN4=
 github.com/pierrec/lz4/v4 v4.1.19/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
@@ -498,6 +528,7 @@ github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529/go.mod h1:qe5TWALJ8/a1
 github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
 github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
 github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
+github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
@@ -512,6 +543,12 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1
 github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
 github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
+github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
@@ -538,6 +575,7 @@ github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDW
 github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
 github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
 github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
+github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
 github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
 github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
 github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
@@ -551,6 +589,7 @@ github.com/willscott/go-nfs v0.0.1 h1:392gV283iuisKFeV9hkKwTdCRfizP+R9FC+gYg2skj
 github.com/willscott/go-nfs v0.0.1/go.mod h1:hBPyqKNde3v8rzxDVWtloP6MtLnx/7aVz3XxxP89W7k=
 github.com/willscott/go-nfs-client v0.0.0-20200605172546-271fa9065b33 h1:Wd8wdpRzPXskyHvZLyw7Wc1fp5oCE2mhBCj7bAiibUs=
 github.com/willscott/go-nfs-client v0.0.0-20200605172546-271fa9065b33/go.mod h1:cOUKSNty+RabZqKhm5yTJT5Vq/Fe83ZRWAJ5Kj8nRes=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
 github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI=
 github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -580,6 +619,7 @@ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUu
 golang.org/x/arch v0.6.0 h1:S0JTfE48HbRj80+4tbvZDYsJ3tGv6BUU3XxyZ7CirAc=
 golang.org/x/arch v0.6.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -642,6 +682,7 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
 golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191011234655-491137f69257/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -685,6 +726,7 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -693,9 +735,11 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
diff --git a/src/host/repository/repository.go b/src/host/repository/repository.go
new file mode 100644
index 0000000..aac2021
--- /dev/null
+++ b/src/host/repository/repository.go
@@ -0,0 +1,90 @@
+package repository
+
+import (
+	"errors"
+	"sync"
+
+	"github.com/anacrolix/torrent/metainfo"
+	"github.com/philippgille/gokv"
+	"github.com/philippgille/gokv/badgerdb"
+	"github.com/philippgille/gokv/encoding"
+)
+
+type TorrentMetaRepository interface {
+	ExcludeFile(hash metainfo.Hash, file ...string) error
+	ExcludedFiles(hash metainfo.Hash) ([]string, error)
+}
+
+func NewTorrentMetaRepository(dir string) (TorrentMetaRepository, error) {
+	store, err := badgerdb.NewStore(badgerdb.Options{
+		Dir:   dir,
+		Codec: encoding.JSON,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	r := &torrentRepositoryImpl{
+		store: store,
+	}
+
+	return r, nil
+}
+
+type torrentRepositoryImpl struct {
+	m     sync.RWMutex
+	store gokv.Store
+}
+
+type torrentMeta struct {
+	ExludedFiles []string
+}
+
+var ErrNotFound = errors.New("not found")
+
+func (r *torrentRepositoryImpl) ExcludeFile(hash metainfo.Hash, file ...string) error {
+	r.m.Lock()
+	defer r.m.Unlock()
+
+	var meta torrentMeta
+	found, err := r.store.Get(hash.AsString(), &meta)
+	if err != nil {
+		return err
+	}
+	if !found {
+		meta = torrentMeta{
+			ExludedFiles: file,
+		}
+	}
+	meta.ExludedFiles = unique(append(meta.ExludedFiles, file...))
+
+	return r.store.Set(hash.AsString(), meta)
+}
+
+func (r *torrentRepositoryImpl) ExcludedFiles(hash metainfo.Hash) ([]string, error) {
+	r.m.Lock()
+	defer r.m.Unlock()
+
+	var meta torrentMeta
+	found, err := r.store.Get(hash.AsString(), &meta)
+	if err != nil {
+		return nil, err
+	}
+	if !found {
+		return nil, nil
+	}
+
+	return meta.ExludedFiles, nil
+}
+
+func unique[C comparable](intSlice []C) []C {
+	keys := make(map[C]bool)
+	list := []C{}
+	for _, entry := range intSlice {
+		if _, value := keys[entry]; !value {
+			keys[entry] = true
+			list = append(list, entry)
+		}
+	}
+	return list
+}
diff --git a/src/host/torrent/service.go b/src/host/torrent/service.go
index 7a074de..a3bc382 100644
--- a/src/host/torrent/service.go
+++ b/src/host/torrent/service.go
@@ -6,6 +6,7 @@ import (
 	"log/slog"
 	"time"
 
+	"git.kmsign.ru/royalcat/tstor/src/host/repository"
 	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
 	"github.com/anacrolix/torrent"
 	"github.com/anacrolix/torrent/metainfo"
@@ -13,7 +14,8 @@ import (
 )
 
 type Service struct {
-	c *torrent.Client
+	c   *torrent.Client
+	rep repository.TorrentMetaRepository
 
 	// stats *Stats
 	DefaultPriority types.PiecePriority
@@ -22,12 +24,13 @@ type Service struct {
 	addTimeout, readTimeout int
 }
 
-func NewService(c *torrent.Client, addTimeout, readTimeout int) *Service {
+func NewService(c *torrent.Client, rep repository.TorrentMetaRepository, addTimeout, readTimeout int) *Service {
 	l := slog.With("component", "torrent-service")
 	return &Service{
 		log:             l,
 		c:               c,
 		DefaultPriority: types.PiecePriorityNone,
+		rep:             rep,
 		// stats:       newStats(), // TODO persistent
 		addTimeout:  addTimeout,
 		readTimeout: readTimeout,
@@ -63,7 +66,7 @@ func (s *Service) NewTorrentFs(f vfs.File) (vfs.Filesystem, error) {
 		t.AllowDataDownload()
 	}
 
-	return vfs.NewTorrentFs(t, s.readTimeout), nil
+	return vfs.NewTorrentFs(t, s.rep, s.readTimeout), nil
 }
 
 func (s *Service) Stats() (*Stats, error) {
diff --git a/src/host/vfs/archive.go b/src/host/vfs/archive.go
index 585f7f9..f4e5649 100644
--- a/src/host/vfs/archive.go
+++ b/src/host/vfs/archive.go
@@ -49,7 +49,10 @@ func NewArchive(r iio.Reader, size int64, loader ArchiveLoader) *archive {
 	}
 }
 
-var _ Filesystem = &archive{}
+// Unlink implements Filesystem.
+func (a *archive) Unlink(filename string) error {
+	return ErrNotImplemented
+}
 
 func (a *archive) Open(filename string) (File, error) {
 	files, err := a.files()
diff --git a/src/host/vfs/fs.go b/src/host/vfs/fs.go
index 3cfc1fc..5f1951b 100644
--- a/src/host/vfs/fs.go
+++ b/src/host/vfs/fs.go
@@ -30,6 +30,7 @@ type Filesystem interface {
 	ReadDir(path string) ([]fs.DirEntry, error)
 
 	Stat(filename string) (fs.FileInfo, error)
+	Unlink(filename string) error
 }
 
 const defaultMode = fs.FileMode(0555)
diff --git a/src/host/vfs/memory.go b/src/host/vfs/memory.go
index eada6e0..2ef42fb 100644
--- a/src/host/vfs/memory.go
+++ b/src/host/vfs/memory.go
@@ -12,6 +12,11 @@ type MemoryFs struct {
 	files map[string]*MemoryFile
 }
 
+// Unlink implements Filesystem.
+func (fs *MemoryFs) Unlink(filename string) error {
+	return ErrNotImplemented
+}
+
 func NewMemoryFS(files map[string]*MemoryFile) *MemoryFs {
 	return &MemoryFs{
 		files: files,
diff --git a/src/host/vfs/os.go b/src/host/vfs/os.go
index 4d09b05..3ec6ad5 100644
--- a/src/host/vfs/os.go
+++ b/src/host/vfs/os.go
@@ -11,6 +11,11 @@ type OsFS struct {
 	hostDir string
 }
 
+// Unlink implements Filesystem.
+func (fs *OsFS) Unlink(filename string) error {
+	return fs.Unlink(filename)
+}
+
 // Stat implements Filesystem.
 func (fs *OsFS) Stat(filename string) (fs.FileInfo, error) {
 	if path.Clean(filename) == Separator {
diff --git a/src/host/vfs/resolver.go b/src/host/vfs/resolver.go
index 9bab3b8..919fe50 100644
--- a/src/host/vfs/resolver.go
+++ b/src/host/vfs/resolver.go
@@ -72,6 +72,19 @@ func (r *ResolveFS) Stat(filename string) (fs.FileInfo, error) {
 	return r.rootFS.Stat(fsPath)
 }
 
+// Unlink implements Filesystem.
+func (r *ResolveFS) Unlink(filename string) error {
+	fsPath, nestedFs, nestedFsPath, err := r.resolver.resolvePath(filename, r.rootFS.Open)
+	if err != nil {
+		return err
+	}
+	if nestedFs != nil {
+		return nestedFs.Unlink(nestedFsPath)
+	}
+
+	return r.rootFS.Unlink(fsPath)
+}
+
 var _ Filesystem = &ResolveFS{}
 
 type FsFactory func(f File) (Filesystem, error)
diff --git a/src/host/vfs/resolver_test.go b/src/host/vfs/resolver_test.go
index ce2af5b..aa6882e 100644
--- a/src/host/vfs/resolver_test.go
+++ b/src/host/vfs/resolver_test.go
@@ -52,6 +52,10 @@ func (d *DummyFs) Open(filename string) (File, error) {
 	return &Dummy{}, nil
 }
 
+func (d *DummyFs) Unlink(filename string) error {
+	return ErrNotImplemented
+}
+
 func (d *DummyFs) ReadDir(path string) ([]fs.DirEntry, error) {
 	if path == "/dir/here" {
 		return []fs.DirEntry{
diff --git a/src/host/vfs/torrent.go b/src/host/vfs/torrent.go
index e4d9247..ddb248c 100644
--- a/src/host/vfs/torrent.go
+++ b/src/host/vfs/torrent.go
@@ -5,19 +5,23 @@ import (
 	"io"
 	"io/fs"
 	"path"
+	"slices"
 	"sync"
 	"time"
 
+	"git.kmsign.ru/royalcat/tstor/src/host/repository"
 	"git.kmsign.ru/royalcat/tstor/src/iio"
 	"github.com/anacrolix/missinggo/v2"
 	"github.com/anacrolix/torrent"
+	"golang.org/x/exp/maps"
 )
 
 var _ Filesystem = &TorrentFs{}
 
 type TorrentFs struct {
-	mu sync.Mutex
-	t  *torrent.Torrent
+	mu  sync.Mutex
+	t   *torrent.Torrent
+	rep repository.TorrentMetaRepository
 
 	readTimeout int
 
@@ -27,22 +31,34 @@ type TorrentFs struct {
 	resolver *resolver
 }
 
-func NewTorrentFs(t *torrent.Torrent, readTimeout int) *TorrentFs {
+func NewTorrentFs(t *torrent.Torrent, rep repository.TorrentMetaRepository, readTimeout int) *TorrentFs {
 	return &TorrentFs{
 		t:           t,
+		rep:         rep,
 		readTimeout: readTimeout,
 		resolver:    newResolver(ArchiveFactories),
 	}
 }
 
-func (fs *TorrentFs) files() map[string]*torrentFile {
+func (fs *TorrentFs) files() (map[string]*torrentFile, error) {
 	if fs.filesCache == nil {
 		fs.mu.Lock()
 		<-fs.t.GotInfo()
 		files := fs.t.Files()
+
+		excludedFiles, err := fs.rep.ExcludedFiles(fs.t.InfoHash())
+		if err != nil {
+			return nil, err
+		}
+
 		fs.filesCache = make(map[string]*torrentFile)
 		for _, file := range files {
 			p := AbsPath(file.Path())
+
+			if slices.Contains(excludedFiles, p) {
+				continue
+			}
+
 			fs.filesCache[p] = &torrentFile{
 				name:       path.Base(p),
 				readerFunc: file.NewReader,
@@ -53,16 +69,24 @@ func (fs *TorrentFs) files() map[string]*torrentFile {
 		fs.mu.Unlock()
 	}
 
-	return fs.filesCache
+	return fs.filesCache, nil
 }
 
 func (fs *TorrentFs) rawOpen(path string) (File, error) {
-	file, err := getFile(fs.files(), path)
+	files, err := fs.files()
+	if err != nil {
+		return nil, err
+	}
+	file, err := getFile(files, path)
 	return file, err
 }
 
 func (fs *TorrentFs) rawStat(filename string) (fs.FileInfo, error) {
-	file, err := getFile(fs.files(), filename)
+	files, err := fs.files()
+	if err != nil {
+		return nil, err
+	}
+	file, err := getFile(files, filename)
 	if err != nil {
 		return nil, err
 	}
@@ -111,14 +135,30 @@ func (fs *TorrentFs) ReadDir(name string) ([]fs.DirEntry, error) {
 	if nestedFs != nil {
 		return nestedFs.ReadDir(nestedFsPath)
 	}
+	files, err := fs.files()
+	if err != nil {
+		return nil, err
+	}
 
-	return listDirFromFiles(fs.files(), fsPath)
+	return listDirFromFiles(files, fsPath)
 }
 
 func (fs *TorrentFs) Unlink(name string) error {
-	file := fs.t.Files()[0]
-	file.SetPriority(torrent.PiecePriorityNone)
-	return nil
+	fs.mu.Lock()
+	defer fs.mu.Unlock()
+
+	files, err := fs.files()
+	if err != nil {
+		return err
+	}
+	file := AbsPath(name)
+
+	if !slices.Contains(maps.Keys(files), file) {
+		return ErrNotExist
+	}
+	fs.filesCache = nil
+
+	return fs.rep.ExcludeFile(fs.t.InfoHash(), file)
 }
 
 type reader interface {
diff --git a/src/mounts/webdav/fs.go b/src/mounts/webdav/fs.go
index 9ff2561..e6411ab 100644
--- a/src/mounts/webdav/fs.go
+++ b/src/mounts/webdav/fs.go
@@ -48,7 +48,7 @@ func (wd *WebDAV) Mkdir(ctx context.Context, name string, perm fs.FileMode) erro
 }
 
 func (wd *WebDAV) RemoveAll(ctx context.Context, name string) error {
-	return webdav.ErrNotImplemented
+	return wd.fs.Unlink(name)
 }
 
 func (wd *WebDAV) Rename(ctx context.Context, oldName, newName string) error {

From cd6cf8dd7423a10f22a386783a7a8273b5210cab Mon Sep 17 00:00:00 2001
From: royalcat <k.adamovich20@gmail.com>
Date: Tue, 26 Dec 2023 22:30:19 +0300
Subject: [PATCH 05/18]  rework exclude repository

---
 cmd/tstor/main.go                 |  5 +---
 src/host/repository/repository.go | 38 ++++++++++++++-----------------
 src/host/torrent/service.go       |  4 ++--
 src/host/vfs/torrent.go           |  4 ++--
 4 files changed, 22 insertions(+), 29 deletions(-)

diff --git a/cmd/tstor/main.go b/cmd/tstor/main.go
index 8ded9b5..fa89413 100644
--- a/cmd/tstor/main.go
+++ b/cmd/tstor/main.go
@@ -73,10 +73,7 @@ func run(configPath string) error {
 		log.Err(err).Msg("set priority failed")
 	}
 
-	if err := os.MkdirAll(filepath.Join(conf.TorrentClient.MetadataFolder, "meta"), 0744); err != nil {
-		return fmt.Errorf("error creating metadata folder: %w", err)
-	}
-	rep, err := repository.NewTorrentMetaRepository(filepath.Join(conf.TorrentClient.MetadataFolder, "meta"))
+	rep, err := repository.NewTorrentMetaRepository(conf.TorrentClient.MetadataFolder)
 	if err != nil {
 		return err
 	}
diff --git a/src/host/repository/repository.go b/src/host/repository/repository.go
index aac2021..e17ed86 100644
--- a/src/host/repository/repository.go
+++ b/src/host/repository/repository.go
@@ -2,6 +2,7 @@ package repository
 
 import (
 	"errors"
+	"path/filepath"
 	"sync"
 
 	"github.com/anacrolix/torrent/metainfo"
@@ -10,34 +11,31 @@ import (
 	"github.com/philippgille/gokv/encoding"
 )
 
-type TorrentMetaRepository interface {
+type TorrentsRepository interface {
 	ExcludeFile(hash metainfo.Hash, file ...string) error
 	ExcludedFiles(hash metainfo.Hash) ([]string, error)
 }
 
-func NewTorrentMetaRepository(dir string) (TorrentMetaRepository, error) {
-	store, err := badgerdb.NewStore(badgerdb.Options{
-		Dir:   dir,
+func NewTorrentMetaRepository(dir string) (TorrentsRepository, error) {
+	excludedFilesStore, err := badgerdb.NewStore(badgerdb.Options{
+		Dir:   filepath.Join(dir, "excluded-files"),
 		Codec: encoding.JSON,
 	})
+
 	if err != nil {
 		return nil, err
 	}
 
 	r := &torrentRepositoryImpl{
-		store: store,
+		excludedFiles: excludedFilesStore,
 	}
 
 	return r, nil
 }
 
 type torrentRepositoryImpl struct {
-	m     sync.RWMutex
-	store gokv.Store
-}
-
-type torrentMeta struct {
-	ExludedFiles []string
+	m             sync.RWMutex
+	excludedFiles gokv.Store
 }
 
 var ErrNotFound = errors.New("not found")
@@ -46,27 +44,25 @@ func (r *torrentRepositoryImpl) ExcludeFile(hash metainfo.Hash, file ...string)
 	r.m.Lock()
 	defer r.m.Unlock()
 
-	var meta torrentMeta
-	found, err := r.store.Get(hash.AsString(), &meta)
+	var excludedFiles []string
+	found, err := r.excludedFiles.Get(hash.AsString(), &excludedFiles)
 	if err != nil {
 		return err
 	}
 	if !found {
-		meta = torrentMeta{
-			ExludedFiles: file,
-		}
+		excludedFiles = []string{}
 	}
-	meta.ExludedFiles = unique(append(meta.ExludedFiles, file...))
+	excludedFiles = unique(append(excludedFiles, file...))
 
-	return r.store.Set(hash.AsString(), meta)
+	return r.excludedFiles.Set(hash.AsString(), excludedFiles)
 }
 
 func (r *torrentRepositoryImpl) ExcludedFiles(hash metainfo.Hash) ([]string, error) {
 	r.m.Lock()
 	defer r.m.Unlock()
 
-	var meta torrentMeta
-	found, err := r.store.Get(hash.AsString(), &meta)
+	var excludedFiles []string
+	found, err := r.excludedFiles.Get(hash.AsString(), &excludedFiles)
 	if err != nil {
 		return nil, err
 	}
@@ -74,7 +70,7 @@ func (r *torrentRepositoryImpl) ExcludedFiles(hash metainfo.Hash) ([]string, err
 		return nil, nil
 	}
 
-	return meta.ExludedFiles, nil
+	return excludedFiles, nil
 }
 
 func unique[C comparable](intSlice []C) []C {
diff --git a/src/host/torrent/service.go b/src/host/torrent/service.go
index a3bc382..2f1761f 100644
--- a/src/host/torrent/service.go
+++ b/src/host/torrent/service.go
@@ -15,7 +15,7 @@ import (
 
 type Service struct {
 	c   *torrent.Client
-	rep repository.TorrentMetaRepository
+	rep repository.TorrentsRepository
 
 	// stats *Stats
 	DefaultPriority types.PiecePriority
@@ -24,7 +24,7 @@ type Service struct {
 	addTimeout, readTimeout int
 }
 
-func NewService(c *torrent.Client, rep repository.TorrentMetaRepository, addTimeout, readTimeout int) *Service {
+func NewService(c *torrent.Client, rep repository.TorrentsRepository, addTimeout, readTimeout int) *Service {
 	l := slog.With("component", "torrent-service")
 	return &Service{
 		log:             l,
diff --git a/src/host/vfs/torrent.go b/src/host/vfs/torrent.go
index ddb248c..88f8b4f 100644
--- a/src/host/vfs/torrent.go
+++ b/src/host/vfs/torrent.go
@@ -21,7 +21,7 @@ var _ Filesystem = &TorrentFs{}
 type TorrentFs struct {
 	mu  sync.Mutex
 	t   *torrent.Torrent
-	rep repository.TorrentMetaRepository
+	rep repository.TorrentsRepository
 
 	readTimeout int
 
@@ -31,7 +31,7 @@ type TorrentFs struct {
 	resolver *resolver
 }
 
-func NewTorrentFs(t *torrent.Torrent, rep repository.TorrentMetaRepository, readTimeout int) *TorrentFs {
+func NewTorrentFs(t *torrent.Torrent, rep repository.TorrentsRepository, readTimeout int) *TorrentFs {
 	return &TorrentFs{
 		t:           t,
 		rep:         rep,

From 5f8d497de165eba94e53f3d16538a58f45950529 Mon Sep 17 00:00:00 2001
From: royalcat <k.adamovich20@gmail.com>
Date: Wed, 27 Dec 2023 00:14:36 +0300
Subject: [PATCH 06/18] rename

---
 cmd/tstor/main.go                            | 8 ++++----
 src/{mounts => export}/fuse/handler.go       | 0
 src/{mounts => export}/fuse/handler_nocgo.go | 0
 src/{mounts => export}/fuse/mount.go         | 0
 src/{mounts => export}/fuse/mount_test.go    | 0
 src/{mounts => export}/httpfs/httpfs.go      | 0
 src/{mounts => export}/nfs/handler.go        | 0
 src/{mounts => export}/nfs/wrapper-v3.go     | 0
 src/{mounts => export}/nfs/wrapper-v4.go     | 0
 src/{mounts => export}/webdav/fs.go          | 0
 src/{mounts => export}/webdav/fs_test.go     | 0
 src/{mounts => export}/webdav/handler.go     | 0
 src/{mounts => export}/webdav/http.go        | 0
 13 files changed, 4 insertions(+), 4 deletions(-)
 rename src/{mounts => export}/fuse/handler.go (100%)
 rename src/{mounts => export}/fuse/handler_nocgo.go (100%)
 rename src/{mounts => export}/fuse/mount.go (100%)
 rename src/{mounts => export}/fuse/mount_test.go (100%)
 rename src/{mounts => export}/httpfs/httpfs.go (100%)
 rename src/{mounts => export}/nfs/handler.go (100%)
 rename src/{mounts => export}/nfs/wrapper-v3.go (100%)
 rename src/{mounts => export}/nfs/wrapper-v4.go (100%)
 rename src/{mounts => export}/webdav/fs.go (100%)
 rename src/{mounts => export}/webdav/fs_test.go (100%)
 rename src/{mounts => export}/webdav/handler.go (100%)
 rename src/{mounts => export}/webdav/http.go (100%)

diff --git a/cmd/tstor/main.go b/cmd/tstor/main.go
index fa89413..7db93bc 100644
--- a/cmd/tstor/main.go
+++ b/cmd/tstor/main.go
@@ -21,12 +21,12 @@ import (
 	"github.com/urfave/cli/v2"
 	wnfs "github.com/willscott/go-nfs"
 
+	"git.kmsign.ru/royalcat/tstor/src/export/fuse"
+	"git.kmsign.ru/royalcat/tstor/src/export/httpfs"
+	"git.kmsign.ru/royalcat/tstor/src/export/nfs"
+	"git.kmsign.ru/royalcat/tstor/src/export/webdav"
 	"git.kmsign.ru/royalcat/tstor/src/http"
 	dlog "git.kmsign.ru/royalcat/tstor/src/log"
-	"git.kmsign.ru/royalcat/tstor/src/mounts/fuse"
-	"git.kmsign.ru/royalcat/tstor/src/mounts/httpfs"
-	"git.kmsign.ru/royalcat/tstor/src/mounts/nfs"
-	"git.kmsign.ru/royalcat/tstor/src/mounts/webdav"
 )
 
 const (
diff --git a/src/mounts/fuse/handler.go b/src/export/fuse/handler.go
similarity index 100%
rename from src/mounts/fuse/handler.go
rename to src/export/fuse/handler.go
diff --git a/src/mounts/fuse/handler_nocgo.go b/src/export/fuse/handler_nocgo.go
similarity index 100%
rename from src/mounts/fuse/handler_nocgo.go
rename to src/export/fuse/handler_nocgo.go
diff --git a/src/mounts/fuse/mount.go b/src/export/fuse/mount.go
similarity index 100%
rename from src/mounts/fuse/mount.go
rename to src/export/fuse/mount.go
diff --git a/src/mounts/fuse/mount_test.go b/src/export/fuse/mount_test.go
similarity index 100%
rename from src/mounts/fuse/mount_test.go
rename to src/export/fuse/mount_test.go
diff --git a/src/mounts/httpfs/httpfs.go b/src/export/httpfs/httpfs.go
similarity index 100%
rename from src/mounts/httpfs/httpfs.go
rename to src/export/httpfs/httpfs.go
diff --git a/src/mounts/nfs/handler.go b/src/export/nfs/handler.go
similarity index 100%
rename from src/mounts/nfs/handler.go
rename to src/export/nfs/handler.go
diff --git a/src/mounts/nfs/wrapper-v3.go b/src/export/nfs/wrapper-v3.go
similarity index 100%
rename from src/mounts/nfs/wrapper-v3.go
rename to src/export/nfs/wrapper-v3.go
diff --git a/src/mounts/nfs/wrapper-v4.go b/src/export/nfs/wrapper-v4.go
similarity index 100%
rename from src/mounts/nfs/wrapper-v4.go
rename to src/export/nfs/wrapper-v4.go
diff --git a/src/mounts/webdav/fs.go b/src/export/webdav/fs.go
similarity index 100%
rename from src/mounts/webdav/fs.go
rename to src/export/webdav/fs.go
diff --git a/src/mounts/webdav/fs_test.go b/src/export/webdav/fs_test.go
similarity index 100%
rename from src/mounts/webdav/fs_test.go
rename to src/export/webdav/fs_test.go
diff --git a/src/mounts/webdav/handler.go b/src/export/webdav/handler.go
similarity index 100%
rename from src/mounts/webdav/handler.go
rename to src/export/webdav/handler.go
diff --git a/src/mounts/webdav/http.go b/src/export/webdav/http.go
similarity index 100%
rename from src/mounts/webdav/http.go
rename to src/export/webdav/http.go

From 49444bd70da2bbfb4858e09cfebc258a0e486d10 Mon Sep 17 00:00:00 2001
From: royalcat <k.adamovich20@gmail.com>
Date: Mon, 1 Jan 2024 01:54:55 +0300
Subject: [PATCH 07/18]  file delete on exclude

---
 cmd/tstor/main.go                             |  24 +-
 go.mod                                        |  38 +--
 go.sum                                        |  74 +++--
 src/host/{torrent => service}/service.go      |   8 +-
 src/host/{torrent => service}/stats.go        |   2 +-
 src/host/storage.go                           |   4 +-
 src/host/{torrent => storage}/client.go       |  16 +-
 src/host/{torrent => storage}/id.go           |   2 +-
 .../{torrent => storage}/piece-completion.go  |   2 +-
 .../{repository => storage}/repository.go     |  21 +-
 src/host/storage/storage.go                   |  51 +++
 src/host/storage/storage_files.go             | 302 +++++++++++++++++
 src/host/{torrent => storage}/store.go        |   2 +-
 src/host/torrent/storage.go                   | 306 ------------------
 src/host/vfs/torrent.go                       |  43 +--
 src/host/vfs/torrent_test.go                  |   5 +-
 src/http/api.go                               |   4 +-
 src/http/http.go                              |   4 +-
 src/iio/disk_test.go                          |   2 +-
 19 files changed, 481 insertions(+), 429 deletions(-)
 rename src/host/{torrent => service}/service.go (87%)
 rename src/host/{torrent => service}/stats.go (99%)
 rename src/host/{torrent => storage}/client.go (64%)
 rename src/host/{torrent => storage}/id.go (96%)
 rename src/host/{torrent => storage}/piece-completion.go (99%)
 rename src/host/{repository => storage}/repository.go (73%)
 create mode 100644 src/host/storage/storage.go
 create mode 100644 src/host/storage/storage_files.go
 rename src/host/{torrent => storage}/store.go (99%)
 delete mode 100644 src/host/torrent/storage.go

diff --git a/cmd/tstor/main.go b/cmd/tstor/main.go
index 7db93bc..645e4a5 100644
--- a/cmd/tstor/main.go
+++ b/cmd/tstor/main.go
@@ -14,8 +14,8 @@ import (
 
 	"git.kmsign.ru/royalcat/tstor/src/config"
 	"git.kmsign.ru/royalcat/tstor/src/host"
-	"git.kmsign.ru/royalcat/tstor/src/host/repository"
-	"git.kmsign.ru/royalcat/tstor/src/host/torrent"
+	"git.kmsign.ru/royalcat/tstor/src/host/service"
+	"git.kmsign.ru/royalcat/tstor/src/host/storage"
 	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
 	"github.com/rs/zerolog/log"
 	"github.com/urfave/cli/v2"
@@ -73,40 +73,40 @@ func run(configPath string) error {
 		log.Err(err).Msg("set priority failed")
 	}
 
-	rep, err := repository.NewTorrentMetaRepository(conf.TorrentClient.MetadataFolder)
-	if err != nil {
-		return err
-	}
-
 	if err := os.MkdirAll(conf.TorrentClient.MetadataFolder, 0744); err != nil {
 		return fmt.Errorf("error creating metadata folder: %w", err)
 	}
 
-	fis, err := torrent.NewFileItemStore(filepath.Join(conf.TorrentClient.MetadataFolder, "items"), 2*time.Hour)
+	fis, err := storage.NewFileItemStore(filepath.Join(conf.TorrentClient.MetadataFolder, "items"), 2*time.Hour)
 	if err != nil {
 		return fmt.Errorf("error starting item store: %w", err)
 	}
 	defer fis.Close()
 
-	id, err := torrent.GetOrCreatePeerID(filepath.Join(conf.TorrentClient.MetadataFolder, "ID"))
+	id, err := storage.GetOrCreatePeerID(filepath.Join(conf.TorrentClient.MetadataFolder, "ID"))
 	if err != nil {
 		return fmt.Errorf("error creating node ID: %w", err)
 	}
 
-	st, _, err := torrent.SetupStorage(conf.TorrentClient)
+	st, _, err := storage.SetupStorage(conf.TorrentClient)
 	if err != nil {
 		return err
 	}
 	defer st.Close()
 
-	c, err := torrent.NewClient(st, fis, &conf.TorrentClient, id)
+	rep, err := storage.NewTorrentMetaRepository(conf.TorrentClient.MetadataFolder, st)
+	if err != nil {
+		return err
+	}
+
+	c, err := storage.NewClient(st, fis, &conf.TorrentClient, id)
 	if err != nil {
 		return fmt.Errorf("error starting torrent client: %w", err)
 	}
 	c.AddDhtNodes(conf.TorrentClient.DHTNodes)
 	defer c.Close()
 
-	ts := torrent.NewService(c, rep, conf.TorrentClient.AddTimeout, conf.TorrentClient.ReadTimeout)
+	ts := service.NewService(c, rep, conf.TorrentClient.AddTimeout, conf.TorrentClient.ReadTimeout)
 
 	if err := os.MkdirAll(conf.DataFolder, 0744); err != nil {
 		return fmt.Errorf("error creating data folder: %w", err)
diff --git a/go.mod b/go.mod
index 5ad4fc0..77d8869 100644
--- a/go.mod
+++ b/go.mod
@@ -11,7 +11,6 @@ require (
 	github.com/billziss-gh/cgofuse v1.5.0
 	github.com/bodgit/sevenzip v1.4.5
 	github.com/dgraph-io/badger/v4 v4.2.0
-	github.com/edsrzf/mmap-go v1.1.0
 	github.com/gin-contrib/pprof v1.4.0
 	github.com/gin-gonic/gin v1.9.1
 	github.com/go-git/go-billy/v5 v5.5.0
@@ -24,20 +23,20 @@ require (
 	github.com/nwaples/rardecode/v2 v2.0.0-beta.2
 	github.com/philippgille/gokv v0.6.0
 	github.com/philippgille/gokv/badgerdb v0.6.0
-	github.com/philippgille/gokv/encoding v0.0.0-20191011213304-eb77f15b9c61
+	github.com/philippgille/gokv/encoding v0.6.0
 	github.com/rs/zerolog v1.31.0
 	github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c
 	github.com/stretchr/testify v1.8.4
-	github.com/urfave/cli/v2 v2.26.0
+	github.com/urfave/cli/v2 v2.27.0
 	github.com/willscott/go-nfs v0.0.1
-	golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611
+	golang.org/x/exp v0.0.0-20231226003508-02704c960a9b
 	golang.org/x/net v0.19.0
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1
 )
 
 require (
 	github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
-	github.com/RoaringBitmap/roaring v1.6.0 // indirect
+	github.com/RoaringBitmap/roaring v1.7.0 // indirect
 	github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect
 	github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
 	github.com/anacrolix/chansync v0.3.0 // indirect
@@ -54,7 +53,7 @@ require (
 	github.com/andybalholm/brotli v1.0.6 // indirect
 	github.com/bahlo/generic-list-go v0.2.0 // indirect
 	github.com/benbjohnson/immutable v0.4.3 // indirect
-	github.com/bits-and-blooms/bitset v1.12.0 // indirect
+	github.com/bits-and-blooms/bitset v1.13.0 // indirect
 	github.com/bodgit/plumbing v1.3.0 // indirect
 	github.com/bodgit/windows v1.0.1 // indirect
 	github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
@@ -64,17 +63,18 @@ require (
 	github.com/chenzhuoyu/iasm v0.9.1 // indirect
 	github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/dgraph-io/badger v1.6.0 // indirect
+	github.com/dgraph-io/badger v1.6.2 // indirect
 	github.com/dgraph-io/ristretto v0.1.1 // indirect
-	github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 // indirect
+	github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
 	github.com/dustin/go-humanize v1.0.1 // indirect
+	github.com/edsrzf/mmap-go v1.1.0 // indirect
 	github.com/fatih/structs v1.1.0 // indirect
 	github.com/fsnotify/fsnotify v1.7.0 // indirect
 	github.com/gabriel-vasile/mimetype v1.4.3 // indirect
 	github.com/gin-contrib/sse v0.1.0 // indirect
-	github.com/go-llsqlite/adapter v0.0.0-20230927005056-7f5ce7f0c916 // indirect
-	github.com/go-llsqlite/crawshaw v0.4.0 // indirect
-	github.com/go-logr/logr v1.3.0 // indirect
+	github.com/go-llsqlite/adapter v0.1.0 // indirect
+	github.com/go-llsqlite/crawshaw v0.5.0 // indirect
+	github.com/go-logr/logr v1.4.1 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-playground/locales v0.14.1 // indirect
 	github.com/go-playground/universal-translator v0.18.1 // indirect
@@ -106,7 +106,7 @@ require (
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/mschoch/smat v0.2.0 // indirect
 	github.com/pelletier/go-toml/v2 v2.1.1 // indirect
-	github.com/philippgille/gokv/util v0.0.0-20191011213304-eb77f15b9c61 // indirect
+	github.com/philippgille/gokv/util v0.6.0 // indirect
 	github.com/pierrec/lz4/v4 v4.1.19 // indirect
 	github.com/pion/datachannel v1.5.5 // indirect
 	github.com/pion/dtls/v2 v2.2.8 // indirect
@@ -144,16 +144,16 @@ require (
 	go.opentelemetry.io/otel/trace v1.21.0 // indirect
 	go4.org v0.0.0-20230225012048-214862532bf5 // indirect
 	golang.org/x/arch v0.6.0 // indirect
-	golang.org/x/crypto v0.16.0 // indirect
+	golang.org/x/crypto v0.17.0 // indirect
 	golang.org/x/sync v0.5.0 // indirect
 	golang.org/x/sys v0.15.0 // indirect
 	golang.org/x/text v0.14.0 // indirect
 	golang.org/x/time v0.5.0 // indirect
-	google.golang.org/protobuf v1.31.0 // indirect
+	google.golang.org/protobuf v1.32.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	modernc.org/libc v1.22.3 // indirect
-	modernc.org/mathutil v1.5.0 // indirect
-	modernc.org/memory v1.5.0 // indirect
-	modernc.org/sqlite v1.21.1 // indirect
-	zombiezen.com/go/sqlite v0.13.1 // indirect
+	modernc.org/libc v1.38.0 // indirect
+	modernc.org/mathutil v1.6.0 // indirect
+	modernc.org/memory v1.7.2 // indirect
+	modernc.org/sqlite v1.28.0 // indirect
+	zombiezen.com/go/sqlite v1.0.0 // indirect
 )
diff --git a/go.sum b/go.sum
index fb686fc..c83405c 100644
--- a/go.sum
+++ b/go.sum
@@ -24,11 +24,12 @@ github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIo
 github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w=
 github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI=
 github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
-github.com/RoaringBitmap/roaring v1.6.0 h1:dc7kRiroETgJcHhWX6BerXkZz2b3JgLGg9nTURJL/og=
-github.com/RoaringBitmap/roaring v1.6.0/go.mod h1:plvDsJQpxOC5bw8LRteu/MLWHsHez/3y6cubLI4/1yE=
+github.com/RoaringBitmap/roaring v1.7.0 h1:OZF303tJCER1Tj3x+aArx/S5X7hrT186ri6JjrGvG68=
+github.com/RoaringBitmap/roaring v1.7.0/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
 github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
 github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
 github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 h1:byYvvbfSo3+9efR4IeReh77gVs4PnNDR3AMOE9NJ7a0=
@@ -112,9 +113,9 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/billziss-gh/cgofuse v1.5.0 h1:kH516I/s+Ab4diL/Y/ayFeUjjA8ey+JK12xDfBf4HEs=
 github.com/billziss-gh/cgofuse v1.5.0/go.mod h1:LJjoaUojlVjgo5GQoEJTcJNqZJeRU0nCR84CyxKt2YM=
-github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
-github.com/bits-and-blooms/bitset v1.12.0 h1:U/q1fAF7xXRhFCrhROzIfffYnu+dlS38vCZtmFVPHmA=
 github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
+github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
+github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
 github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
 github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
 github.com/bodgit/sevenzip v1.4.5 h1:HFJQ+nbjppfyf2xbQEJBbmVo+o2kTg1FXV4i7YOx87s=
@@ -130,6 +131,7 @@ github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ
 github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=
 github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
 github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -156,14 +158,17 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/dgraph-io/badger v1.6.0 h1:DshxFxZWXUcO0xX476VJC07Xsr6ZCBVRHKZ93Oh7Evo=
 github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
+github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8=
+github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE=
 github.com/dgraph-io/badger/v4 v4.2.0 h1:kJrlajbXXL9DFTNuhhu9yCx7JJa4qpYWxtE8BzuWsEs=
 github.com/dgraph-io/badger/v4 v4.2.0/go.mod h1:qfCqhPoWDFJRx1gp5QwwyGo8xk1lbHUxvK9nK0OGAak=
+github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
 github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
 github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
-github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
 github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
+github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
+github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
 github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
 github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
@@ -209,15 +214,15 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-llsqlite/adapter v0.0.0-20230927005056-7f5ce7f0c916 h1:OyQmpAN302wAopDgwVjgs2HkFawP9ahIEqkUYz7V7CA=
-github.com/go-llsqlite/adapter v0.0.0-20230927005056-7f5ce7f0c916/go.mod h1:DADrR88ONKPPeSGjFp5iEN55Arx3fi2qXZeKCYDpbmU=
-github.com/go-llsqlite/crawshaw v0.4.0 h1:L02s2jZBBJj80xm1VkkdyB/JlQ/Fi0kLbNHfXA8yrec=
-github.com/go-llsqlite/crawshaw v0.4.0/go.mod h1:/YJdV7uBQaYDE0fwe4z3wwJIZBJxdYzd38ICggWqtaE=
+github.com/go-llsqlite/adapter v0.1.0 h1:wGSQNsu/rtYeu/lqZNZQMjwUdEF3OW66xTLvsFwJQUw=
+github.com/go-llsqlite/adapter v0.1.0/go.mod h1:DADrR88ONKPPeSGjFp5iEN55Arx3fi2qXZeKCYDpbmU=
+github.com/go-llsqlite/crawshaw v0.5.0 h1:Olbqkth53vkkh4WvmkYjrtfOBcxXD3rMYBYuk6FNH3E=
+github.com/go-llsqlite/crawshaw v0.5.0/go.mod h1:/YJdV7uBQaYDE0fwe4z3wwJIZBJxdYzd38ICggWqtaE=
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
-github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
+github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
@@ -431,12 +436,14 @@ github.com/philippgille/gokv v0.6.0 h1:fNEx/tSwV73nzlYd3iRYB8F+SEVJNNFzH1gsaT8SK
 github.com/philippgille/gokv v0.6.0/go.mod h1:tjXRFw9xDHgxLS8WJdfYotKGWp8TWqu4RdXjMDG/XBo=
 github.com/philippgille/gokv/badgerdb v0.6.0 h1:4Qigf2SpyXLF8KaM5nA5/D/0aD/bZevuAnrW4ZsDsjA=
 github.com/philippgille/gokv/badgerdb v0.6.0/go.mod h1:3u2avs8gtmCc0R0Bw4jKV8aaDfLb5V9JToSASyhpFGM=
-github.com/philippgille/gokv/encoding v0.0.0-20191011213304-eb77f15b9c61 h1:IgQDuUPuEFVf22mBskeCLAtvd5c9XiiJG2UYud6eGHI=
 github.com/philippgille/gokv/encoding v0.0.0-20191011213304-eb77f15b9c61/go.mod h1:SjxSrCoeYrYn85oTtroyG1ePY8aE72nvLQlw8IYwAN8=
+github.com/philippgille/gokv/encoding v0.6.0 h1:P1TN+Aulpd6Qd7qcLqgPwoxzOQ42UHBXOovWvFxJRI8=
+github.com/philippgille/gokv/encoding v0.6.0/go.mod h1:/yKvq2BKJlKJsH7KMDrhDlEw2Pt3V1nKyFhs4iOqz5U=
 github.com/philippgille/gokv/test v0.0.0-20191011213304-eb77f15b9c61 h1:4tVyBgfpK0NSqu7tNZTwYfC/pbyWUR2y+O7mxEg5BTQ=
 github.com/philippgille/gokv/test v0.0.0-20191011213304-eb77f15b9c61/go.mod h1:EUc+s9ONc1+VOr9NUEd8S0YbGRrQd/gz/p+2tvwt12s=
-github.com/philippgille/gokv/util v0.0.0-20191011213304-eb77f15b9c61 h1:ril/jI0JgXNjPWwDkvcRxlZ09kgHXV2349xChjbsQ4o=
 github.com/philippgille/gokv/util v0.0.0-20191011213304-eb77f15b9c61/go.mod h1:2dBhsJgY/yVIkjY5V3AnDUxUbEPzT6uQ3LvoVT8TR20=
+github.com/philippgille/gokv/util v0.6.0 h1:GrTxVENzKBxs8lB3tnaA88mKOuVPT7atZPplxX+PNmo=
+github.com/philippgille/gokv/util v0.6.0/go.mod h1:ovoDHZ2Svr7YX972SPPJQRXbhHEy3Gb20HRH/Tr9BiQ=
 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
 github.com/pierrec/lz4/v4 v4.1.19 h1:tYLzDnjDXh9qIxSTKHwXwOYmm9d887Y7Y1ZkyXYHAN4=
 github.com/pierrec/lz4/v4 v4.1.19/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
@@ -514,7 +521,6 @@ github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4
 github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93 h1:UVArwN/wkKjMVhh2EQGC0tEc1+FqiLlvYXY5mQ2f8Wg=
 github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93/go.mod h1:Nfe4efndBz4TibWycNE+lqyJZiMX4ycx+QKV8Ta0f/o=
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
-github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@@ -543,6 +549,8 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1
 github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
 github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
 github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
 github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
 github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
@@ -581,8 +589,8 @@ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65E
 github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
 github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
 github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
-github.com/urfave/cli/v2 v2.26.0 h1:3f3AMg3HpThFNT4I++TKOejZO8yU55t3JnnSr4S4QEI=
-github.com/urfave/cli/v2 v2.26.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
+github.com/urfave/cli/v2 v2.27.0 h1:uNs1K8JwTFL84X68j5Fjny6hfANh9nTlJ6dRtZAFAHY=
+github.com/urfave/cli/v2 v2.27.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
 github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
 github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
 github.com/willscott/go-nfs v0.0.1 h1:392gV283iuisKFeV9hkKwTdCRfizP+R9FC+gYg2skj0=
@@ -632,8 +640,8 @@ golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45
 golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
 golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
 golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
-golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
-golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
+golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -643,8 +651,8 @@ golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u0
 golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
-golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 h1:qCEDpW1G+vcj3Y7Fy52pEM1AWm3abj8WimGYejI3SC4=
-golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
+golang.org/x/exp v0.0.0-20231226003508-02704c960a9b h1:kLiC65FbiHWFAOu+lxwNPujcsl8VYyTYYEZnsOO1WK4=
+golang.org/x/exp v0.0.0-20231226003508-02704c960a9b/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -889,8 +897,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
-google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
+google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -918,18 +926,18 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
-modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY=
-modernc.org/libc v1.22.3/go.mod h1:MQrloYP209xa2zHome2a8HLiLm6k0UT8CoHpV74tOFw=
-modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
-modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
-modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
-modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
-modernc.org/sqlite v1.21.1 h1:GyDFqNnESLOhwwDRaHGdp2jKLDzpyT/rNLglX3ZkMSU=
-modernc.org/sqlite v1.21.1/go.mod h1:XwQ0wZPIh1iKb5mkvCJ3szzbhk+tykC8ZWqTRTgYRwI=
+modernc.org/libc v1.38.0 h1:o4Lpk0zNDSdsjfEXnF1FGXWQ9PDi1NOdWcLP5n13FGo=
+modernc.org/libc v1.38.0/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
+modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
+modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
+modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
+modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
+modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
+modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
 nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
 rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
-zombiezen.com/go/sqlite v0.13.1 h1:qDzxyWWmMtSSEH5qxamqBFmqA2BLSSbtODi3ojaE02o=
-zombiezen.com/go/sqlite v0.13.1/go.mod h1:Ht/5Rg3Ae2hoyh1I7gbWtWAl89CNocfqeb/aAMTkJr4=
+zombiezen.com/go/sqlite v1.0.0 h1:D2EvOZqumJBy+6t+0uNTTXnepUpB/pKG45op/UziI1o=
+zombiezen.com/go/sqlite v1.0.0/go.mod h1:Yx7FJ77tr7Ucwi5solhXAxpflyxk/BHNXArZ/JvDm60=
diff --git a/src/host/torrent/service.go b/src/host/service/service.go
similarity index 87%
rename from src/host/torrent/service.go
rename to src/host/service/service.go
index 2f1761f..43e9caa 100644
--- a/src/host/torrent/service.go
+++ b/src/host/service/service.go
@@ -1,4 +1,4 @@
-package torrent
+package service
 
 import (
 	"context"
@@ -6,7 +6,7 @@ import (
 	"log/slog"
 	"time"
 
-	"git.kmsign.ru/royalcat/tstor/src/host/repository"
+	"git.kmsign.ru/royalcat/tstor/src/host/storage"
 	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
 	"github.com/anacrolix/torrent"
 	"github.com/anacrolix/torrent/metainfo"
@@ -15,7 +15,7 @@ import (
 
 type Service struct {
 	c   *torrent.Client
-	rep repository.TorrentsRepository
+	rep storage.TorrentsRepository
 
 	// stats *Stats
 	DefaultPriority types.PiecePriority
@@ -24,7 +24,7 @@ type Service struct {
 	addTimeout, readTimeout int
 }
 
-func NewService(c *torrent.Client, rep repository.TorrentsRepository, addTimeout, readTimeout int) *Service {
+func NewService(c *torrent.Client, rep storage.TorrentsRepository, addTimeout, readTimeout int) *Service {
 	l := slog.With("component", "torrent-service")
 	return &Service{
 		log:             l,
diff --git a/src/host/torrent/stats.go b/src/host/service/stats.go
similarity index 99%
rename from src/host/torrent/stats.go
rename to src/host/service/stats.go
index 9b6f956..85c53dd 100644
--- a/src/host/torrent/stats.go
+++ b/src/host/service/stats.go
@@ -1,4 +1,4 @@
-package torrent
+package service
 
 import (
 	"errors"
diff --git a/src/host/storage.go b/src/host/storage.go
index c080d72..bc5bfb0 100644
--- a/src/host/storage.go
+++ b/src/host/storage.go
@@ -1,11 +1,11 @@
 package host
 
 import (
-	"git.kmsign.ru/royalcat/tstor/src/host/torrent"
+	"git.kmsign.ru/royalcat/tstor/src/host/service"
 	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
 )
 
-func NewStorage(dataPath string, tsrv *torrent.Service) vfs.Filesystem {
+func NewStorage(dataPath string, tsrv *service.Service) vfs.Filesystem {
 	factories := map[string]vfs.FsFactory{
 		".torrent": tsrv.NewTorrentFs,
 	}
diff --git a/src/host/torrent/client.go b/src/host/storage/client.go
similarity index 64%
rename from src/host/torrent/client.go
rename to src/host/storage/client.go
index d432883..bacdc15 100644
--- a/src/host/torrent/client.go
+++ b/src/host/storage/client.go
@@ -1,4 +1,4 @@
-package torrent
+package storage
 
 import (
 	"time"
@@ -20,20 +20,6 @@ func NewClient(st storage.ClientImpl, fis bep44.Store, cfg *config.TorrentClient
 	torrentCfg.PeerID = string(id[:])
 	torrentCfg.DefaultStorage = st
 
-	// torrentCfg.DisableIPv6 = cfg.DisableIPv6
-	// torrentCfg.DropDuplicatePeerIds = true
-	// torrentCfg.TorrentPeersLowWater = 10
-	// torrentCfg.TorrentPeersHighWater = 100
-	// torrentCfg.DisableWebtorrent = true
-	// torrentCfg.DisableAggressiveUpload = true
-	// torrentCfg.DisableWebseeds = true
-	// torrentCfg.DisableUTP = false
-	// torrentCfg.NoDefaultPortForwarding = true
-	// torrentCfg.AlwaysWantConns = false
-	// torrentCfg.ClientDhtConfig = torrent.ClientDhtConfig{
-	// 	NoDHT: true,
-	// }
-
 	l := log.Logger.With().Str("component", "torrent-client").Logger()
 
 	tl := tlog.NewLogger()
diff --git a/src/host/torrent/id.go b/src/host/storage/id.go
similarity index 96%
rename from src/host/torrent/id.go
rename to src/host/storage/id.go
index e18471c..9422893 100644
--- a/src/host/torrent/id.go
+++ b/src/host/storage/id.go
@@ -1,4 +1,4 @@
-package torrent
+package storage
 
 import (
 	"crypto/rand"
diff --git a/src/host/torrent/piece-completion.go b/src/host/storage/piece-completion.go
similarity index 99%
rename from src/host/torrent/piece-completion.go
rename to src/host/storage/piece-completion.go
index 4b7eeec..b98ebaa 100644
--- a/src/host/torrent/piece-completion.go
+++ b/src/host/storage/piece-completion.go
@@ -1,4 +1,4 @@
-package torrent
+package storage
 
 import (
 	"encoding/binary"
diff --git a/src/host/repository/repository.go b/src/host/storage/repository.go
similarity index 73%
rename from src/host/repository/repository.go
rename to src/host/storage/repository.go
index e17ed86..b988485 100644
--- a/src/host/repository/repository.go
+++ b/src/host/storage/repository.go
@@ -1,10 +1,11 @@
-package repository
+package storage
 
 import (
 	"errors"
 	"path/filepath"
 	"sync"
 
+	"github.com/anacrolix/torrent"
 	"github.com/anacrolix/torrent/metainfo"
 	"github.com/philippgille/gokv"
 	"github.com/philippgille/gokv/badgerdb"
@@ -12,13 +13,13 @@ import (
 )
 
 type TorrentsRepository interface {
-	ExcludeFile(hash metainfo.Hash, file ...string) error
+	ExcludeFile(file *torrent.File) error
 	ExcludedFiles(hash metainfo.Hash) ([]string, error)
 }
 
-func NewTorrentMetaRepository(dir string) (TorrentsRepository, error) {
+func NewTorrentMetaRepository(metaDir string, storage *FileStorage) (TorrentsRepository, error) {
 	excludedFilesStore, err := badgerdb.NewStore(badgerdb.Options{
-		Dir:   filepath.Join(dir, "excluded-files"),
+		Dir:   filepath.Join(metaDir, "excluded-files"),
 		Codec: encoding.JSON,
 	})
 
@@ -28,6 +29,7 @@ func NewTorrentMetaRepository(dir string) (TorrentsRepository, error) {
 
 	r := &torrentRepositoryImpl{
 		excludedFiles: excludedFilesStore,
+		storage:       storage,
 	}
 
 	return r, nil
@@ -36,14 +38,16 @@ func NewTorrentMetaRepository(dir string) (TorrentsRepository, error) {
 type torrentRepositoryImpl struct {
 	m             sync.RWMutex
 	excludedFiles gokv.Store
+	storage       *FileStorage
 }
 
 var ErrNotFound = errors.New("not found")
 
-func (r *torrentRepositoryImpl) ExcludeFile(hash metainfo.Hash, file ...string) error {
+func (r *torrentRepositoryImpl) ExcludeFile(file *torrent.File) error {
 	r.m.Lock()
 	defer r.m.Unlock()
 
+	hash := file.Torrent().InfoHash()
 	var excludedFiles []string
 	found, err := r.excludedFiles.Get(hash.AsString(), &excludedFiles)
 	if err != nil {
@@ -52,7 +56,12 @@ func (r *torrentRepositoryImpl) ExcludeFile(hash metainfo.Hash, file ...string)
 	if !found {
 		excludedFiles = []string{}
 	}
-	excludedFiles = unique(append(excludedFiles, file...))
+	excludedFiles = unique(append(excludedFiles, file.Path()))
+
+	err = r.storage.DeleteFile(file)
+	if err != nil {
+		return err
+	}
 
 	return r.excludedFiles.Set(hash.AsString(), excludedFiles)
 }
diff --git a/src/host/storage/storage.go b/src/host/storage/storage.go
new file mode 100644
index 0000000..b1719ac
--- /dev/null
+++ b/src/host/storage/storage.go
@@ -0,0 +1,51 @@
+package storage
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+
+	"git.kmsign.ru/royalcat/tstor/src/config"
+	"github.com/anacrolix/torrent/storage"
+)
+
+func SetupStorage(cfg config.TorrentClient) (*FileStorage, storage.PieceCompletion, error) {
+	pcp := filepath.Join(cfg.DataFolder, "piece-completion")
+	if err := os.MkdirAll(pcp, 0744); err != nil {
+		return nil, nil, fmt.Errorf("error creating piece completion folder: %w", err)
+	}
+	pc, err := storage.NewBoltPieceCompletion(pcp)
+	if err != nil {
+		return nil, nil, fmt.Errorf("error creating servers piece completion: %w", err)
+	}
+
+	// pc, err := NewBadgerPieceCompletion(pcp)
+	// if err != nil {
+	// 	return nil, nil, fmt.Errorf("error creating servers piece completion: %w", err)
+	// }
+
+	// TODO implement cache/storage switching
+	// cacheDir := filepath.Join(tcfg.DataFolder, "cache")
+	// if err := os.MkdirAll(cacheDir, 0744); err != nil {
+	// 	return nil, nil, fmt.Errorf("error creating piece completion folder: %w", err)
+	// }
+	// fc, err := filecache.NewCache(cacheDir)
+	// if err != nil {
+	// 	return nil, nil, fmt.Errorf("error creating cache: %w", err)
+	// }
+	// log.Info().Msg(fmt.Sprintf("setting cache size to %d MB", 1024))
+	// fc.SetCapacity(1024 * 1024 * 1024)
+
+	// rp := storage.NewResourcePieces(fc.AsResourceProvider())
+	// st := &stc{rp}
+
+	filesDir := filepath.Join(cfg.DataFolder, "files")
+	if err := os.MkdirAll(pcp, 0744); err != nil {
+		return nil, nil, fmt.Errorf("error creating piece completion folder: %w", err)
+	}
+
+	// st := storage.NewMMapWithCompletion(filesDir, pc)
+	st := NewFileStorage(filesDir, pc)
+
+	return st, pc, nil
+}
diff --git a/src/host/storage/storage_files.go b/src/host/storage/storage_files.go
new file mode 100644
index 0000000..e6d109f
--- /dev/null
+++ b/src/host/storage/storage_files.go
@@ -0,0 +1,302 @@
+package storage
+
+import (
+	"fmt"
+	"io"
+	"log"
+	"os"
+	"path"
+	"path/filepath"
+	"strings"
+
+	"github.com/anacrolix/missinggo"
+	"github.com/anacrolix/torrent"
+	"github.com/anacrolix/torrent/common"
+	"github.com/anacrolix/torrent/metainfo"
+	"github.com/anacrolix/torrent/segments"
+	"github.com/anacrolix/torrent/storage"
+)
+
+// NewFileStorage creates a new ClientImplCloser that stores files using the OS native filesystem.
+func NewFileStorage(baseDir string, pc storage.PieceCompletion) *FileStorage {
+	return &FileStorage{baseDir: baseDir, pieceCompletion: pc}
+}
+
+// File-based storage for torrents, that isn't yet bound to a particular torrent.
+type FileStorage struct {
+	baseDir         string
+	pieceCompletion storage.PieceCompletion
+}
+
+func (me *FileStorage) Close() error {
+	return me.pieceCompletion.Close()
+}
+
+func (me *FileStorage) torrentDir(info *metainfo.Info, infoHash metainfo.Hash) string {
+	return filepath.Join(me.baseDir, info.Name)
+}
+
+func (me *FileStorage) filePath(file metainfo.FileInfo) string {
+	return filepath.Join(file.Path...)
+}
+
+func (fs *FileStorage) DeleteFile(file *torrent.File) error {
+	info := file.Torrent().Info()
+	infoHash := file.Torrent().InfoHash()
+	torrentDir := fs.torrentDir(info, infoHash)
+	relFilePath := fs.filePath(file.FileInfo())
+	filePath := path.Join(torrentDir, relFilePath)
+	return os.Remove(filePath)
+}
+
+func (fs FileStorage) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (storage.TorrentImpl, error) {
+	dir := fs.torrentDir(info, infoHash)
+	upvertedFiles := info.UpvertedFiles()
+	files := make([]file, 0, len(upvertedFiles))
+	for i, fileInfo := range upvertedFiles {
+		filePath := filepath.Join(dir, fs.filePath(fileInfo))
+		if !isSubFilepath(dir, filePath) {
+			return storage.TorrentImpl{}, fmt.Errorf("file %v: path %q is not sub path of %q", i, filePath, fs.baseDir)
+		}
+
+		f := file{
+			path:   filePath,
+			length: fileInfo.Length,
+		}
+		if f.length == 0 {
+			err := CreateNativeZeroLengthFile(f.path)
+			if err != nil {
+				return storage.TorrentImpl{}, fmt.Errorf("creating zero length file: %w", err)
+			}
+		}
+		files = append(files, f)
+	}
+	t := &fileTorrentImpl{
+		files:          files,
+		segmentLocater: segments.NewIndex(common.LengthIterFromUpvertedFiles(upvertedFiles)),
+		infoHash:       infoHash,
+		completion:     fs.pieceCompletion,
+	}
+	return storage.TorrentImpl{
+		Piece: t.Piece,
+		Close: t.Close,
+	}, nil
+}
+
+type file struct {
+	// The safe, OS-local file path.
+	path   string
+	length int64
+}
+
+type fileTorrentImpl struct {
+	files          []file
+	segmentLocater segments.Index
+	infoHash       metainfo.Hash
+	completion     storage.PieceCompletion
+}
+
+func (fts *fileTorrentImpl) Piece(p metainfo.Piece) storage.PieceImpl {
+	// Create a view onto the file-based torrent storage.
+	_io := fileTorrentImplIO{fts}
+	// Return the appropriate segments of this.
+	return &filePieceImpl{
+		fileTorrentImpl: fts,
+		p:               p,
+		WriterAt:        missinggo.NewSectionWriter(_io, p.Offset(), p.Length()),
+		ReaderAt:        io.NewSectionReader(_io, p.Offset(), p.Length()),
+	}
+}
+
+func (fs *fileTorrentImpl) Close() error {
+	return nil
+}
+
+// A helper to create zero-length files which won't appear for file-orientated storage since no
+// writes will ever occur to them (no torrent data is associated with a zero-length file). The
+// caller should make sure the file name provided is safe/sanitized.
+func CreateNativeZeroLengthFile(name string) error {
+	err := os.MkdirAll(filepath.Dir(name), 0o777)
+	if err != nil {
+		return err
+	}
+	f, err := os.Create(name)
+	if err != nil {
+		return err
+	}
+	return f.Close()
+}
+
+// Exposes file-based storage of a torrent, as one big ReadWriterAt.
+type fileTorrentImplIO struct {
+	fts *fileTorrentImpl
+}
+
+// Returns EOF on short or missing file.
+func (fst *fileTorrentImplIO) readFileAt(file file, b []byte, off int64) (n int, err error) {
+	f, err := os.Open(file.path)
+	if os.IsNotExist(err) {
+		// File missing is treated the same as a short file.
+		err = io.EOF
+		return
+	}
+	if err != nil {
+		return
+	}
+	defer f.Close()
+	// Limit the read to within the expected bounds of this file.
+	if int64(len(b)) > file.length-off {
+		b = b[:file.length-off]
+	}
+	for off < file.length && len(b) != 0 {
+		n1, err1 := f.ReadAt(b, off)
+		b = b[n1:]
+		n += n1
+		off += int64(n1)
+		if n1 == 0 {
+			err = err1
+			break
+		}
+	}
+	return
+}
+
+// Only returns EOF at the end of the torrent. Premature EOF is ErrUnexpectedEOF.
+func (fst fileTorrentImplIO) ReadAt(b []byte, off int64) (n int, err error) {
+	fst.fts.segmentLocater.Locate(
+		segments.Extent{Start: off, Length: int64(len(b))},
+		func(i int, e segments.Extent) bool {
+			n1, err1 := fst.readFileAt(fst.fts.files[i], b[:e.Length], e.Start)
+			n += n1
+			b = b[n1:]
+			err = err1
+			return err == nil // && int64(n1) == e.Length
+		},
+	)
+	if len(b) != 0 && err == nil {
+		err = io.EOF
+	}
+	return
+}
+
+func (fst fileTorrentImplIO) WriteAt(p []byte, off int64) (n int, err error) {
+	// log.Printf("write at %v: %v bytes", off, len(p))
+	fst.fts.segmentLocater.Locate(
+		segments.Extent{Start: off, Length: int64(len(p))},
+		func(i int, e segments.Extent) bool {
+			name := fst.fts.files[i].path
+			err = os.MkdirAll(filepath.Dir(name), 0o777)
+			if err != nil {
+				return false
+			}
+			var f *os.File
+			f, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0o666)
+			if err != nil {
+				return false
+			}
+			var n1 int
+			n1, err = f.WriteAt(p[:e.Length], e.Start)
+			// log.Printf("%v %v wrote %v: %v", i, e, n1, err)
+			closeErr := f.Close()
+			n += n1
+			p = p[n1:]
+			if err == nil {
+				err = closeErr
+			}
+			if err == nil && int64(n1) != e.Length {
+				err = io.ErrShortWrite
+			}
+			return err == nil
+		},
+	)
+	return n, err
+}
+
+type filePieceImpl struct {
+	*fileTorrentImpl
+	p metainfo.Piece
+	io.WriterAt
+	io.ReaderAt
+}
+
+var _ storage.PieceImpl = (*filePieceImpl)(nil)
+
+func (me *filePieceImpl) pieceKey() metainfo.PieceKey {
+	return metainfo.PieceKey{InfoHash: me.infoHash, Index: me.p.Index()}
+}
+
+func (fs *filePieceImpl) Completion() storage.Completion {
+	c, err := fs.completion.Get(fs.pieceKey())
+	if err != nil {
+		log.Printf("error getting piece completion: %s", err)
+		c.Ok = false
+		return c
+	}
+
+	verified := true
+	if c.Complete {
+		// If it's allegedly complete, check that its constituent files have the necessary length.
+		for _, fi := range extentCompleteRequiredLengths(fs.p.Info, fs.p.Offset(), fs.p.Length()) {
+			s, err := os.Stat(fs.files[fi.fileIndex].path)
+			if err != nil || s.Size() < fi.length {
+				verified = false
+				break
+			}
+		}
+	}
+
+	if !verified {
+		// The completion was wrong, fix it.
+		c.Complete = false
+		fs.completion.Set(fs.pieceKey(), false)
+	}
+
+	return c
+}
+
+func (fs *filePieceImpl) MarkComplete() error {
+	return fs.completion.Set(fs.pieceKey(), true)
+}
+
+func (fs *filePieceImpl) MarkNotComplete() error {
+	return fs.completion.Set(fs.pieceKey(), false)
+}
+
+type requiredLength struct {
+	fileIndex int
+	length    int64
+}
+
+func isSubFilepath(base, sub string) bool {
+	rel, err := filepath.Rel(base, sub)
+	if err != nil {
+		return false
+	}
+	return rel != ".." && !strings.HasPrefix(rel, ".."+string(os.PathSeparator))
+}
+
+func extentCompleteRequiredLengths(info *metainfo.Info, off, n int64) (ret []requiredLength) {
+	if n == 0 {
+		return
+	}
+	for i, fi := range info.UpvertedFiles() {
+		if off >= fi.Length {
+			off -= fi.Length
+			continue
+		}
+		n1 := n
+		if off+n1 > fi.Length {
+			n1 = fi.Length - off
+		}
+		ret = append(ret, requiredLength{
+			fileIndex: i,
+			length:    off + n1,
+		})
+		n -= n1
+		if n == 0 {
+			return
+		}
+		off = 0
+	}
+	panic("extent exceeds torrent bounds")
+}
diff --git a/src/host/torrent/store.go b/src/host/storage/store.go
similarity index 99%
rename from src/host/torrent/store.go
rename to src/host/storage/store.go
index f762671..0f9adf2 100644
--- a/src/host/torrent/store.go
+++ b/src/host/storage/store.go
@@ -1,4 +1,4 @@
-package torrent
+package storage
 
 import (
 	"bytes"
diff --git a/src/host/torrent/storage.go b/src/host/torrent/storage.go
deleted file mode 100644
index 24f694a..0000000
--- a/src/host/torrent/storage.go
+++ /dev/null
@@ -1,306 +0,0 @@
-package torrent
-
-import (
-	"errors"
-	"fmt"
-	"io"
-	"os"
-	"path/filepath"
-
-	"git.kmsign.ru/royalcat/tstor/src/config"
-	"github.com/anacrolix/missinggo"
-	"github.com/anacrolix/torrent"
-	"github.com/anacrolix/torrent/metainfo"
-	"github.com/anacrolix/torrent/mmap_span"
-	"github.com/anacrolix/torrent/storage"
-	"github.com/edsrzf/mmap-go"
-)
-
-type Torrent struct {
-	client *torrent.Client
-	data   storage.ClientImplCloser
-	pc     storage.PieceCompletion
-}
-
-func SetupStorage(cfg config.TorrentClient) (storage.ClientImplCloser, storage.PieceCompletion, error) {
-	pcp := filepath.Join(cfg.DataFolder, "piece-completion")
-	if err := os.MkdirAll(pcp, 0744); err != nil {
-		return nil, nil, fmt.Errorf("error creating piece completion folder: %w", err)
-	}
-	pc, err := storage.NewBoltPieceCompletion(pcp)
-	if err != nil {
-		return nil, nil, fmt.Errorf("error creating servers piece completion: %w", err)
-	}
-
-	// pc, err := NewBadgerPieceCompletion(pcp)
-	// if err != nil {
-	// 	return nil, nil, fmt.Errorf("error creating servers piece completion: %w", err)
-	// }
-
-	// TODO implement cache/storage switching
-	// cacheDir := filepath.Join(tcfg.DataFolder, "cache")
-	// if err := os.MkdirAll(cacheDir, 0744); err != nil {
-	// 	return nil, nil, fmt.Errorf("error creating piece completion folder: %w", err)
-	// }
-	// fc, err := filecache.NewCache(cacheDir)
-	// if err != nil {
-	// 	return nil, nil, fmt.Errorf("error creating cache: %w", err)
-	// }
-	// log.Info().Msg(fmt.Sprintf("setting cache size to %d MB", 1024))
-	// fc.SetCapacity(1024 * 1024 * 1024)
-
-	// rp := storage.NewResourcePieces(fc.AsResourceProvider())
-	// st := &stc{rp}
-
-	filesDir := filepath.Join(cfg.DataFolder, "files")
-	if err := os.MkdirAll(pcp, 0744); err != nil {
-		return nil, nil, fmt.Errorf("error creating piece completion folder: %w", err)
-	}
-
-	// st := storage.NewMMapWithCompletion(filesDir, pc)
-	st := storage.NewFileOpts(storage.NewFileClientOpts{
-		ClientBaseDir:   filesDir,
-		PieceCompletion: pc,
-	})
-
-	return st, pc, nil
-}
-
-func (s Torrent) Remove(f *torrent.File) error {
-
-	return nil
-}
-
-// type dupePieces struct {
-// }
-
-// func (s Torrent) dedupe(f1, f2 *os.File) error {
-// 	for _, t := range s.client.Torrents() {
-// 		for i := 0; i < t.NumPieces(); i++ {
-// 			p := t.Piece(i)
-// 			p.Info().Hash()
-// 		}
-// 	}
-
-// 	// https://go-review.googlesource.com/c/sys/+/284352/10/unix/syscall_linux_test.go#856
-// 	// dedupe := unix.FileDedupeRange{
-// 	// 	Src_offset: uint64(0),
-// 	// 	Src_length: uint64(4096),
-// 	// 	Info: []unix.FileDedupeRangeInfo{
-// 	// 		unix.FileDedupeRangeInfo{
-// 	// 			Dest_fd:     int64(f2.Fd()),
-// 	// 			Dest_offset: uint64(0),
-// 	// 		},
-// 	// 		unix.FileDedupeRangeInfo{
-// 	// 			Dest_fd:     int64(f2.Fd()),
-// 	// 			Dest_offset: uint64(4096),
-// 	// 		},
-// 	// 	}}
-// 	// err := unix.IoctlFileDedupeRange(int(f1.Fd()), &dedupe)
-// 	// if err == unix.EOPNOTSUPP || err == unix.EINVAL {
-// 	// 	t.Skip("deduplication not supported on this filesystem")
-// 	// } else if err != nil {
-// 	// 	t.Fatal(err)
-// 	// }
-
-// 	return nil
-// }
-
-type mmapClientImpl struct {
-	baseDir string
-	pc      storage.PieceCompletion
-}
-
-func NewMMapWithCompletion(baseDir string, completion storage.PieceCompletion) *mmapClientImpl {
-	return &mmapClientImpl{
-		baseDir: baseDir,
-		pc:      completion,
-	}
-}
-
-func (s *mmapClientImpl) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (_ storage.TorrentImpl, err error) {
-	t, err := newMMapTorrent(info, infoHash, s.baseDir, s.pc)
-	if err != nil {
-		return storage.TorrentImpl{}, err
-	}
-	return storage.TorrentImpl{Piece: t.Piece, Close: t.Close, Flush: t.Flush}, nil
-}
-
-func (s *mmapClientImpl) Close() error {
-	return s.pc.Close()
-}
-
-func newMMapTorrent(md *metainfo.Info, infoHash metainfo.Hash, location string, pc storage.PieceCompletionGetSetter) (*mmapTorrent, error) {
-	span := &mmap_span.MMapSpan{}
-	basePath, err := storage.ToSafeFilePath(md.Name)
-	if err != nil {
-		return nil, err
-	}
-	basePath = filepath.Join(location, basePath)
-
-	for _, miFile := range md.UpvertedFiles() {
-		var safeName string
-		safeName, err = storage.ToSafeFilePath(miFile.Path...)
-		if err != nil {
-			return nil, err
-		}
-		fileName := filepath.Join(basePath, safeName)
-		var mm FileMapping
-		mm, err = mmapFile(fileName, miFile.Length)
-		if err != nil {
-			err = fmt.Errorf("file %q: %s", miFile.DisplayPath(md), err)
-			return nil, err
-		}
-		span.Append(mm)
-	}
-	span.InitIndex()
-
-	return &mmapTorrent{
-		infoHash: infoHash,
-		span:     span,
-		pc:       pc,
-	}, nil
-}
-
-type mmapTorrent struct {
-	infoHash metainfo.Hash
-	span     *mmap_span.MMapSpan
-	pc       storage.PieceCompletionGetSetter
-}
-
-func (ts *mmapTorrent) Piece(p metainfo.Piece) storage.PieceImpl {
-	return mmapPiece{
-		pc:       ts.pc,
-		p:        p,
-		ih:       ts.infoHash,
-		ReaderAt: io.NewSectionReader(ts.span, p.Offset(), p.Length()),
-		WriterAt: missinggo.NewSectionWriter(ts.span, p.Offset(), p.Length()),
-	}
-}
-
-func (ts *mmapTorrent) Close() error {
-	errs := ts.span.Close()
-	if len(errs) > 0 {
-		return errs[0]
-	}
-	return nil
-}
-
-func (ts *mmapTorrent) Flush() error {
-	errs := ts.span.Flush()
-	if len(errs) > 0 {
-		return errs[0]
-	}
-	return nil
-}
-
-type mmapPiece struct {
-	pc storage.PieceCompletionGetSetter
-	p  metainfo.Piece
-	ih metainfo.Hash
-	io.ReaderAt
-	io.WriterAt
-}
-
-func (me mmapPiece) pieceKey() metainfo.PieceKey {
-	return metainfo.PieceKey{InfoHash: me.ih, Index: me.p.Index()}
-}
-
-func (sp mmapPiece) Completion() storage.Completion {
-	c, err := sp.pc.Get(sp.pieceKey())
-	if err != nil {
-		panic(err)
-	}
-	return c
-}
-
-func (sp mmapPiece) MarkComplete() error {
-	return sp.pc.Set(sp.pieceKey(), true)
-}
-
-func (sp mmapPiece) MarkNotComplete() error {
-	return sp.pc.Set(sp.pieceKey(), false)
-}
-
-func mmapFile(name string, size int64) (_ FileMapping, err error) {
-	dir := filepath.Dir(name)
-	err = os.MkdirAll(dir, 0o750)
-	if err != nil {
-		return nil, fmt.Errorf("making directory %q: %s", dir, err)
-	}
-	var file *os.File
-	file, err = os.OpenFile(name, os.O_CREATE|os.O_RDWR, 0o666)
-	if err != nil {
-		return nil, err
-	}
-	defer func() {
-		if err != nil {
-			file.Close()
-		}
-	}()
-	var fi os.FileInfo
-	fi, err = file.Stat()
-	if err != nil {
-		return nil, err
-	}
-	if fi.Size() < size {
-		// I think this is necessary on HFS+. Maybe Linux will SIGBUS too if
-		// you overmap a file but I'm not sure.
-		err = file.Truncate(size)
-		if err != nil {
-			return nil, err
-		}
-	}
-	return func() (ret mmapWithFile, err error) {
-		ret.f = file
-		if size == 0 {
-			// Can't mmap() regions with length 0.
-			return
-		}
-		intLen := int(size)
-		if int64(intLen) != size {
-			err = errors.New("size too large for system")
-			return
-		}
-		ret.mmap, err = mmap.MapRegion(file, intLen, mmap.RDWR, 0, 0)
-		if err != nil {
-			err = fmt.Errorf("error mapping region: %s", err)
-			return
-		}
-		if int64(len(ret.mmap)) != size {
-			panic(len(ret.mmap))
-		}
-		return
-	}()
-}
-
-type FileMapping = mmap_span.Mmap
-
-// Handles closing the mmap's file handle (needed for Windows). Could be implemented differently by
-// OS.
-type mmapWithFile struct {
-	f    *os.File
-	mmap mmap.MMap
-}
-
-func (m mmapWithFile) Flush() error {
-	return m.mmap.Flush()
-}
-
-func (m mmapWithFile) Unmap() (err error) {
-	if m.mmap != nil {
-		err = m.mmap.Unmap()
-	}
-	fileErr := m.f.Close()
-	if err == nil {
-		err = fileErr
-	}
-	return
-}
-
-func (m mmapWithFile) Bytes() []byte {
-	if m.mmap == nil {
-		return nil
-	}
-	return m.mmap
-}
diff --git a/src/host/vfs/torrent.go b/src/host/vfs/torrent.go
index 88f8b4f..ccf38ef 100644
--- a/src/host/vfs/torrent.go
+++ b/src/host/vfs/torrent.go
@@ -9,7 +9,7 @@ import (
 	"sync"
 	"time"
 
-	"git.kmsign.ru/royalcat/tstor/src/host/repository"
+	"git.kmsign.ru/royalcat/tstor/src/host/storage"
 	"git.kmsign.ru/royalcat/tstor/src/iio"
 	"github.com/anacrolix/missinggo/v2"
 	"github.com/anacrolix/torrent"
@@ -21,7 +21,7 @@ var _ Filesystem = &TorrentFs{}
 type TorrentFs struct {
 	mu  sync.Mutex
 	t   *torrent.Torrent
-	rep repository.TorrentsRepository
+	rep storage.TorrentsRepository
 
 	readTimeout int
 
@@ -31,7 +31,7 @@ type TorrentFs struct {
 	resolver *resolver
 }
 
-func NewTorrentFs(t *torrent.Torrent, rep repository.TorrentsRepository, readTimeout int) *TorrentFs {
+func NewTorrentFs(t *torrent.Torrent, rep storage.TorrentsRepository, readTimeout int) *TorrentFs {
 	return &TorrentFs{
 		t:           t,
 		rep:         rep,
@@ -53,17 +53,17 @@ func (fs *TorrentFs) files() (map[string]*torrentFile, error) {
 
 		fs.filesCache = make(map[string]*torrentFile)
 		for _, file := range files {
-			p := AbsPath(file.Path())
 
-			if slices.Contains(excludedFiles, p) {
+			if slices.Contains(excludedFiles, file.Path()) {
 				continue
 			}
 
+			p := AbsPath(file.Path())
+
 			fs.filesCache[p] = &torrentFile{
-				name:       path.Base(p),
-				readerFunc: file.NewReader,
-				len:        file.Length(),
-				timeout:    fs.readTimeout,
+				name:    path.Base(p),
+				timeout: fs.readTimeout,
+				file:    file,
 			}
 		}
 		fs.mu.Unlock()
@@ -144,6 +144,8 @@ func (fs *TorrentFs) ReadDir(name string) ([]fs.DirEntry, error) {
 }
 
 func (fs *TorrentFs) Unlink(name string) error {
+	name = AbsPath(name)
+
 	fs.mu.Lock()
 	defer fs.mu.Unlock()
 
@@ -151,14 +153,15 @@ func (fs *TorrentFs) Unlink(name string) error {
 	if err != nil {
 		return err
 	}
-	file := AbsPath(name)
 
-	if !slices.Contains(maps.Keys(files), file) {
+	if !slices.Contains(maps.Keys(files), name) {
 		return ErrNotExist
 	}
-	fs.filesCache = nil
 
-	return fs.rep.ExcludeFile(fs.t.InfoHash(), file)
+	file := files[name]
+	delete(fs.filesCache, name)
+
+	return fs.rep.ExcludeFile(file.file)
 }
 
 type reader interface {
@@ -224,25 +227,25 @@ var _ File = &torrentFile{}
 type torrentFile struct {
 	name string
 
-	readerFunc func() torrent.Reader
-	reader     reader
-	len        int64
-	timeout    int
+	reader  reader
+	timeout int
+
+	file *torrent.File
 }
 
 func (d *torrentFile) Stat() (fs.FileInfo, error) {
-	return newFileInfo(d.name, d.len), nil
+	return newFileInfo(d.name, d.file.Length()), nil
 }
 
 func (d *torrentFile) load() {
 	if d.reader != nil {
 		return
 	}
-	d.reader = newReadAtWrapper(d.readerFunc(), d.timeout)
+	d.reader = newReadAtWrapper(d.file.NewReader(), d.timeout)
 }
 
 func (d *torrentFile) Size() int64 {
-	return d.len
+	return d.file.Length()
 }
 
 func (d *torrentFile) IsDir() bool {
diff --git a/src/host/vfs/torrent_test.go b/src/host/vfs/torrent_test.go
index bb9fdb0..2a8fac9 100644
--- a/src/host/vfs/torrent_test.go
+++ b/src/host/vfs/torrent_test.go
@@ -96,9 +96,8 @@ func TestReadAtTorrent(t *testing.T) {
 	torrFile := to.Files()[0]
 
 	tf := torrentFile{
-		readerFunc: torrFile.NewReader,
-		len:        torrFile.Length(),
-		timeout:    500,
+		file:    torrFile,
+		timeout: 500,
 	}
 
 	defer tf.Close()
diff --git a/src/http/api.go b/src/http/api.go
index c227c2e..9a232e8 100644
--- a/src/http/api.go
+++ b/src/http/api.go
@@ -7,12 +7,12 @@ import (
 	"net/http"
 	"os"
 
-	"git.kmsign.ru/royalcat/tstor/src/host/torrent"
+	"git.kmsign.ru/royalcat/tstor/src/host/service"
 	"github.com/anacrolix/missinggo/v2/filecache"
 	"github.com/gin-gonic/gin"
 )
 
-var apiStatusHandler = func(fc *filecache.Cache, ss *torrent.Stats) gin.HandlerFunc {
+var apiStatusHandler = func(fc *filecache.Cache, ss *service.Stats) gin.HandlerFunc {
 	return func(ctx *gin.Context) {
 		stat := gin.H{
 			"torrentStats": ss.GlobalStats(),
diff --git a/src/http/http.go b/src/http/http.go
index 55a6590..d10064a 100644
--- a/src/http/http.go
+++ b/src/http/http.go
@@ -6,7 +6,7 @@ import (
 
 	"git.kmsign.ru/royalcat/tstor"
 	"git.kmsign.ru/royalcat/tstor/src/config"
-	"git.kmsign.ru/royalcat/tstor/src/host/torrent"
+	"git.kmsign.ru/royalcat/tstor/src/host/service"
 	"github.com/anacrolix/missinggo/v2/filecache"
 	"github.com/gin-contrib/pprof"
 	"github.com/gin-gonic/gin"
@@ -14,7 +14,7 @@ import (
 	"github.com/shurcooL/httpfs/html/vfstemplate"
 )
 
-func New(fc *filecache.Cache, ss *torrent.Stats, s *torrent.Service, logPath string, cfg *config.Config) error {
+func New(fc *filecache.Cache, ss *service.Stats, s *service.Service, logPath string, cfg *config.Config) error {
 	gin.SetMode(gin.ReleaseMode)
 	r := gin.New()
 	r.Use(gin.Recovery())
diff --git a/src/iio/disk_test.go b/src/iio/disk_test.go
index c540d02..5df8814 100644
--- a/src/iio/disk_test.go
+++ b/src/iio/disk_test.go
@@ -26,7 +26,7 @@ func TestReadData(t *testing.T) {
 	require.Equal(5, n)
 	require.Equal("World", string(toRead))
 
-	r.ReadAt(toRead, 0)
+	n, err = r.ReadAt(toRead, 0)
 	require.NoError(err)
 	require.Equal(5, n)
 	require.Equal("Hello", string(toRead))

From 7d9f1a437cdbee5534f8c39d2d2e2cfb747e9c95 Mon Sep 17 00:00:00 2001
From: royalcat <k.adamovich20@gmail.com>
Date: Mon, 1 Jan 2024 21:17:32 +0300
Subject: [PATCH 08/18] qol

---
 cmd/tstor/main.go                 |  2 +-
 src/host/storage/repository.go    | 13 ++++++++-----
 src/host/storage/storage.go       | 16 +++++++++++-----
 src/host/storage/storage_files.go | 14 +++++++++++++-
 src/host/vfs/resolver.go          |  3 +++
 src/host/vfs/torrent.go           | 15 +++++++++++++--
 6 files changed, 49 insertions(+), 14 deletions(-)

diff --git a/cmd/tstor/main.go b/cmd/tstor/main.go
index 645e4a5..3131e8f 100644
--- a/cmd/tstor/main.go
+++ b/cmd/tstor/main.go
@@ -176,7 +176,7 @@ func run(configPath string) error {
 	go func() {
 		logFilename := filepath.Join(conf.Log.Path, dlog.FileName)
 
-		err = http.New(nil, nil, ts, logFilename, conf)
+		err = http.New(nil, service.NewStats(), ts, logFilename, conf)
 		log.Error().Err(err).Msg("error initializing HTTP server")
 	}()
 
diff --git a/src/host/storage/repository.go b/src/host/storage/repository.go
index b988485..07985e1 100644
--- a/src/host/storage/repository.go
+++ b/src/host/storage/repository.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/anacrolix/torrent"
 	"github.com/anacrolix/torrent/metainfo"
+	atstorage "github.com/anacrolix/torrent/storage"
 	"github.com/philippgille/gokv"
 	"github.com/philippgille/gokv/badgerdb"
 	"github.com/philippgille/gokv/encoding"
@@ -17,7 +18,7 @@ type TorrentsRepository interface {
 	ExcludedFiles(hash metainfo.Hash) ([]string, error)
 }
 
-func NewTorrentMetaRepository(metaDir string, storage *FileStorage) (TorrentsRepository, error) {
+func NewTorrentMetaRepository(metaDir string, storage atstorage.ClientImplCloser) (TorrentsRepository, error) {
 	excludedFilesStore, err := badgerdb.NewStore(badgerdb.Options{
 		Dir:   filepath.Join(metaDir, "excluded-files"),
 		Codec: encoding.JSON,
@@ -38,7 +39,7 @@ func NewTorrentMetaRepository(metaDir string, storage *FileStorage) (TorrentsRep
 type torrentRepositoryImpl struct {
 	m             sync.RWMutex
 	excludedFiles gokv.Store
-	storage       *FileStorage
+	storage       atstorage.ClientImplCloser
 }
 
 var ErrNotFound = errors.New("not found")
@@ -58,9 +59,11 @@ func (r *torrentRepositoryImpl) ExcludeFile(file *torrent.File) error {
 	}
 	excludedFiles = unique(append(excludedFiles, file.Path()))
 
-	err = r.storage.DeleteFile(file)
-	if err != nil {
-		return err
+	if storage, ok := r.storage.(FileStorageDeleter); ok {
+		err = storage.DeleteFile(file)
+		if err != nil {
+			return err
+		}
 	}
 
 	return r.excludedFiles.Set(hash.AsString(), excludedFiles)
diff --git a/src/host/storage/storage.go b/src/host/storage/storage.go
index b1719ac..33cfed4 100644
--- a/src/host/storage/storage.go
+++ b/src/host/storage/storage.go
@@ -9,7 +9,7 @@ import (
 	"github.com/anacrolix/torrent/storage"
 )
 
-func SetupStorage(cfg config.TorrentClient) (*FileStorage, storage.PieceCompletion, error) {
+func SetupStorage(cfg config.TorrentClient) (storage.ClientImplCloser, storage.PieceCompletion, error) {
 	pcp := filepath.Join(cfg.DataFolder, "piece-completion")
 	if err := os.MkdirAll(pcp, 0744); err != nil {
 		return nil, nil, fmt.Errorf("error creating piece completion folder: %w", err)
@@ -39,13 +39,19 @@ func SetupStorage(cfg config.TorrentClient) (*FileStorage, storage.PieceCompleti
 	// rp := storage.NewResourcePieces(fc.AsResourceProvider())
 	// st := &stc{rp}
 
-	filesDir := filepath.Join(cfg.DataFolder, "files")
-	if err := os.MkdirAll(pcp, 0744); err != nil {
+	// filesDir := filepath.Join(cfg.DataFolder, "files")
+	// if err := os.MkdirAll(filesDir, 0744); err != nil {
+	// 	return nil, nil, fmt.Errorf("error creating piece completion folder: %w", err)
+	// }
+
+	//st := NewFileStorage(filesDir, pc)
+
+	piecesDir := filepath.Join(cfg.DataFolder, "pieces")
+	if err := os.MkdirAll(piecesDir, 0744); err != nil {
 		return nil, nil, fmt.Errorf("error creating piece completion folder: %w", err)
 	}
 
-	// st := storage.NewMMapWithCompletion(filesDir, pc)
-	st := NewFileStorage(filesDir, pc)
+	st := storage.NewMMapWithCompletion(piecesDir, pc)
 
 	return st, pc, nil
 }
diff --git a/src/host/storage/storage_files.go b/src/host/storage/storage_files.go
index e6d109f..04c837b 100644
--- a/src/host/storage/storage_files.go
+++ b/src/host/storage/storage_files.go
@@ -17,8 +17,13 @@ import (
 	"github.com/anacrolix/torrent/storage"
 )
 
+type FileStorageDeleter interface {
+	storage.ClientImplCloser
+	DeleteFile(file *torrent.File) error
+}
+
 // NewFileStorage creates a new ClientImplCloser that stores files using the OS native filesystem.
-func NewFileStorage(baseDir string, pc storage.PieceCompletion) *FileStorage {
+func NewFileStorage(baseDir string, pc storage.PieceCompletion) FileStorageDeleter {
 	return &FileStorage{baseDir: baseDir, pieceCompletion: pc}
 }
 
@@ -46,6 +51,13 @@ func (fs *FileStorage) DeleteFile(file *torrent.File) error {
 	torrentDir := fs.torrentDir(info, infoHash)
 	relFilePath := fs.filePath(file.FileInfo())
 	filePath := path.Join(torrentDir, relFilePath)
+	for i := file.BeginPieceIndex(); i < file.EndPieceIndex(); i++ {
+		pk := metainfo.PieceKey{InfoHash: infoHash, Index: i}
+		err := fs.pieceCompletion.Set(pk, false)
+		if err != nil {
+			return err
+		}
+	}
 	return os.Remove(filePath)
 }
 
diff --git a/src/host/vfs/resolver.go b/src/host/vfs/resolver.go
index 919fe50..5685d0b 100644
--- a/src/host/vfs/resolver.go
+++ b/src/host/vfs/resolver.go
@@ -204,6 +204,9 @@ func listDirFromFiles[F File](m map[string]F, name string) ([]fs.DirEntry, error
 
 		}
 	}
+	slices.SortStableFunc(out, func(de1, de2 fs.DirEntry) int {
+		return strings.Compare(de1.Name(), de2.Name())
+	})
 	out = slices.CompactFunc(out, func(de1, de2 fs.DirEntry) bool {
 		return de1.Name() == de2.Name()
 	})
diff --git a/src/host/vfs/torrent.go b/src/host/vfs/torrent.go
index ccf38ef..43a6f21 100644
--- a/src/host/vfs/torrent.go
+++ b/src/host/vfs/torrent.go
@@ -6,6 +6,7 @@ import (
 	"io/fs"
 	"path"
 	"slices"
+	"strings"
 	"sync"
 	"time"
 
@@ -54,11 +55,21 @@ func (fs *TorrentFs) files() (map[string]*torrentFile, error) {
 		fs.filesCache = make(map[string]*torrentFile)
 		for _, file := range files {
 
-			if slices.Contains(excludedFiles, file.Path()) {
+			p := file.Path()
+
+			if slices.Contains(excludedFiles, p) {
+				continue
+			}
+			if strings.Contains(p, "/.pad/") {
 				continue
 			}
 
-			p := AbsPath(file.Path())
+			p = AbsPath(file.Path())
+
+			// TODO make optional
+			// removing the torrent root directory of same name  as torrent
+			p, _ = strings.CutPrefix(p, "/"+fs.t.Name()+"/")
+			p = AbsPath(p)
 
 			fs.filesCache[p] = &torrentFile{
 				name:    path.Base(p),

From 78704bee782143fa4490069f0418678bdf10ca23 Mon Sep 17 00:00:00 2001
From: royalcat <k.adamovich20@gmail.com>
Date: Mon, 1 Jan 2024 21:17:40 +0300
Subject: [PATCH 09/18] web

---
 src/host/service/stats.go | 2 +-
 src/http/api.go           | 8 ++++----
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/host/service/stats.go b/src/host/service/stats.go
index 85c53dd..962e9e2 100644
--- a/src/host/service/stats.go
+++ b/src/host/service/stats.go
@@ -79,7 +79,7 @@ type Stats struct {
 	gTime time.Time
 }
 
-func newStats() *Stats {
+func NewStats() *Stats {
 	return &Stats{
 		gTime:    time.Now(),
 		torrents: make(map[string]*torrent.Torrent),
diff --git a/src/http/api.go b/src/http/api.go
index 9a232e8..96fba6a 100644
--- a/src/http/api.go
+++ b/src/http/api.go
@@ -29,7 +29,7 @@ var apiStatusHandler = func(fc *filecache.Cache, ss *service.Stats) gin.HandlerF
 	}
 }
 
-// var apiServersHandler = func(ss []*torrent.Server) gin.HandlerFunc {
+// var apiServersHandler = func(ss []*service.Server) gin.HandlerFunc {
 // 	return func(ctx *gin.Context) {
 // 		var infos []*torrent.ServerInfo
 // 		for _, s := range ss {
@@ -39,7 +39,7 @@ var apiStatusHandler = func(fc *filecache.Cache, ss *service.Stats) gin.HandlerF
 // 	}
 // }
 
-// var apiRoutesHandler = func(ss *torrent.Stats) gin.HandlerFunc {
+// var apiRoutesHandler = func(ss *service.Stats) gin.HandlerFunc {
 // 	return func(ctx *gin.Context) {
 // 		s := ss.RoutesStats()
 // 		sort.Sort(torrent.ByName(s))
@@ -47,7 +47,7 @@ var apiStatusHandler = func(fc *filecache.Cache, ss *service.Stats) gin.HandlerF
 // 	}
 // }
 
-// var apiAddTorrentHandler = func(s *torrent.Service) gin.HandlerFunc {
+// var apiAddTorrentHandler = func(s *service.Service) gin.HandlerFunc {
 // 	return func(ctx *gin.Context) {
 // 		route := ctx.Param("route")
 
@@ -66,7 +66,7 @@ var apiStatusHandler = func(fc *filecache.Cache, ss *service.Stats) gin.HandlerF
 // 	}
 // }
 
-// var apiDelTorrentHandler = func(s *torrent.Service) gin.HandlerFunc {
+// var apiDelTorrentHandler = func(s *service.Service) gin.HandlerFunc {
 // 	return func(ctx *gin.Context) {
 // 		route := ctx.Param("route")
 // 		hash := ctx.Param("torrent_hash")

From 2cefb9db98ea8504a63ca4d2227a82a30a4b9c07 Mon Sep 17 00:00:00 2001
From: royalcat <k.adamovich20@gmail.com>
Date: Sun, 7 Jan 2024 20:09:56 +0300
Subject: [PATCH 10/18] WIP

---
 cmd/tstor/main.go                             |  56 ++-
 go.mod                                        | 144 +++----
 go.sum                                        | 395 ++++++++----------
 src/config/default.go                         |   2 +-
 src/config/model.go                           |   2 +-
 src/export/nfs/handler.go                     |  10 +-
 src/export/nfs/wrapper-v3.go                  |  68 +--
 src/export/webdav/http.go                     |  40 ++
 src/host/service/service.go                   |  38 +-
 src/host/storage.go                           |   2 +-
 .../{repository.go => excluded-files.go}      |   4 +-
 src/host/storage/piece-completion.go          |   6 +-
 src/host/storage/storage.go                   |  27 +-
 src/host/storage/storage_files.go             | 307 +++-----------
 src/host/vfs/os.go                            |  10 +-
 src/host/vfs/torrent.go                       |  28 +-
 src/log/badger.go                             |   3 +
 src/log/nfs.go                                | 173 ++++++++
 src/proto/gen.go                              |   3 +
 19 files changed, 691 insertions(+), 627 deletions(-)
 rename src/host/storage/{repository.go => excluded-files.go} (93%)
 create mode 100644 src/log/nfs.go
 create mode 100644 src/proto/gen.go

diff --git a/cmd/tstor/main.go b/cmd/tstor/main.go
index 3131e8f..0050c20 100644
--- a/cmd/tstor/main.go
+++ b/cmd/tstor/main.go
@@ -16,7 +16,6 @@ import (
 	"git.kmsign.ru/royalcat/tstor/src/host"
 	"git.kmsign.ru/royalcat/tstor/src/host/service"
 	"git.kmsign.ru/royalcat/tstor/src/host/storage"
-	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
 	"github.com/rs/zerolog/log"
 	"github.com/urfave/cli/v2"
 	wnfs "github.com/willscott/go-nfs"
@@ -67,7 +66,9 @@ func run(configPath string) error {
 	}
 
 	dlog.Load(&conf.Log)
+	log := log.Logger.With().Str("conponent", "run").Logger()
 
+	// TODO make optional
 	err = syscall.Setpriority(syscall.PRIO_PGRP, 0, 19)
 	if err != nil {
 		log.Err(err).Msg("set priority failed")
@@ -94,7 +95,7 @@ func run(configPath string) error {
 	}
 	defer st.Close()
 
-	rep, err := storage.NewTorrentMetaRepository(conf.TorrentClient.MetadataFolder, st)
+	rep, err := storage.NewExcludedFiles(conf.TorrentClient.MetadataFolder, st)
 	if err != nil {
 		return err
 	}
@@ -108,14 +109,49 @@ func run(configPath string) error {
 
 	ts := service.NewService(c, rep, conf.TorrentClient.AddTimeout, conf.TorrentClient.ReadTimeout)
 
-	if err := os.MkdirAll(conf.DataFolder, 0744); err != nil {
+	if err := os.MkdirAll(conf.SourceDir, 0744); err != nil {
 		return fmt.Errorf("error creating data folder: %w", err)
 	}
-	cfs := host.NewStorage(conf.DataFolder, ts)
+	sfs := host.NewTorrentStorage(conf.SourceDir, ts)
+
+	// TODO make separate function
+	// {
+	// 	if st, ok := st.(storage.FileStorageDeleter); ok {
+	// 		log.Info().Msg("listing files")
+	// 		files, err := listFilesRecursive(conf.SourceDir)
+	// 		if err != nil {
+	// 			return fmt.Errorf("error listing files: %w", err)
+	// 		}
+
+	// 		torrentFiles := []string{}
+	// 		for _, v := range files {
+	// 			if strings.HasSuffix(v, ".torrent") {
+	// 				torrentFiles = append(torrentFiles, v)
+	// 			}
+	// 		}
+
+	// 		log.Info().Int("count", len(torrentFiles)).Msg("loading torrent files")
+	// 		torrentList := []*torrent.Torrent{}
+	// 		for _, tf := range torrentFiles {
+	// 			t, err := c.AddTorrentFromFile(tf)
+	// 			if err != nil {
+	// 				return err
+	// 			}
+	// 			<-t.GotInfo()
+	// 			torrentList = append(torrentList, t)
+	// 		}
+	// 		log.Info().Msg("staring cleanup")
+	// 		err = st.Cleanup(torrentList)
+	// 		if err != nil {
+	// 			return fmt.Errorf("cleanup error: %w", err)
+	// 		}
+	// 	}
+
+	// }
 
 	if conf.Mounts.Fuse.Enabled {
 		mh := fuse.NewHandler(conf.Mounts.Fuse.AllowOther, conf.Mounts.Fuse.Path)
-		err := mh.Mount(cfs)
+		err := mh.Mount(sfs)
 		if err != nil {
 			return fmt.Errorf("mount fuse error: %w", err)
 		}
@@ -124,7 +160,7 @@ func run(configPath string) error {
 
 	if conf.Mounts.WebDAV.Enabled {
 		go func() {
-			if err := webdav.NewWebDAVServer(cfs, conf.Mounts.WebDAV.Port, conf.Mounts.WebDAV.User, conf.Mounts.WebDAV.Pass); err != nil {
+			if err := webdav.NewWebDAVServer(sfs, conf.Mounts.WebDAV.Port, conf.Mounts.WebDAV.User, conf.Mounts.WebDAV.Pass); err != nil {
 				log.Error().Err(err).Msg("error starting webDAV")
 			}
 
@@ -133,7 +169,7 @@ func run(configPath string) error {
 	}
 	if conf.Mounts.HttpFs.Enabled {
 		go func() {
-			httpfs := httpfs.NewHTTPFS(cfs)
+			httpfs := httpfs.NewHTTPFS(sfs)
 			err = nethttp.ListenAndServe(fmt.Sprintf("0.0.0.0:%d", conf.Mounts.HttpFs.Port), nethttp.FileServer(httpfs))
 			if err != nil {
 				log.Error().Err(err).Msg("error starting HTTPFS")
@@ -157,16 +193,14 @@ func run(configPath string) error {
 			listener, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", conf.Mounts.NFS.Port))
 			panicOnErr(err, "starting TCP listener")
 			log.Info().Str("host", listener.Addr().String()).Msg("starting NFS server")
-			handler, err := nfs.NewNFSv3Handler(cfs)
+			handler, err := nfs.NewNFSv3Handler(sfs)
 			panicOnErr(err, "creating NFS handler")
 			panicOnErr(wnfs.Serve(listener, handler), "serving nfs")
 		}()
 	}
 
-	dataFS := vfs.NewOsFs(conf.DataFolder)
-
 	go func() {
-		if err := webdav.NewWebDAVServer(dataFS, 36912, conf.Mounts.WebDAV.User, conf.Mounts.WebDAV.Pass); err != nil {
+		if err := webdav.NewDirServer(conf.SourceDir, 36912, conf.Mounts.WebDAV.User, conf.Mounts.WebDAV.Pass); err != nil {
 			log.Error().Err(err).Msg("error starting webDAV")
 		}
 
diff --git a/go.mod b/go.mod
index 77d8869..fb88c2c 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,6 @@ go 1.21
 require (
 	github.com/anacrolix/dht/v2 v2.21.0
 	github.com/anacrolix/log v0.14.5
-	github.com/anacrolix/missinggo v1.3.0
 	github.com/anacrolix/missinggo/v2 v2.7.3
 	github.com/anacrolix/torrent v1.53.2
 	github.com/billziss-gh/cgofuse v1.5.0
@@ -28,132 +27,135 @@ require (
 	github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c
 	github.com/stretchr/testify v1.8.4
 	github.com/urfave/cli/v2 v2.27.0
-	github.com/willscott/go-nfs v0.0.1
+	github.com/willscott/go-nfs v0.0.2
 	golang.org/x/exp v0.0.0-20231226003508-02704c960a9b
 	golang.org/x/net v0.19.0
+	google.golang.org/grpc v1.53.0
+	google.golang.org/protobuf v1.30.0
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1
 )
 
 require (
 	github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
-	github.com/RoaringBitmap/roaring v1.7.0 // indirect
+	github.com/RoaringBitmap/roaring v1.2.3 // indirect
 	github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect
 	github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
 	github.com/anacrolix/chansync v0.3.0 // indirect
 	github.com/anacrolix/envpprof v1.3.0 // indirect
-	github.com/anacrolix/generics v0.0.0-20230911070922-5dd7545c6b13 // indirect
+	github.com/anacrolix/generics v0.0.0-20230816105729-c755655aee45 // indirect
 	github.com/anacrolix/go-libutp v1.3.1 // indirect
+	github.com/anacrolix/missinggo v1.3.0 // indirect
 	github.com/anacrolix/missinggo/perf v1.0.0 // indirect
 	github.com/anacrolix/mmsg v1.0.0 // indirect
-	github.com/anacrolix/multiless v0.3.1-0.20230203023154-f3d27407d8f1 // indirect
-	github.com/anacrolix/stm v0.5.0 // indirect
+	github.com/anacrolix/multiless v0.3.1-0.20221221005021-2d12701f83f7 // indirect
+	github.com/anacrolix/stm v0.4.1-0.20221221005312-96d17df0e496 // indirect
 	github.com/anacrolix/sync v0.5.1 // indirect
-	github.com/anacrolix/upnp v0.1.3 // indirect
-	github.com/anacrolix/utp v0.2.0 // indirect
+	github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96 // indirect
+	github.com/anacrolix/utp v0.1.0 // indirect
 	github.com/andybalholm/brotli v1.0.6 // indirect
 	github.com/bahlo/generic-list-go v0.2.0 // indirect
-	github.com/benbjohnson/immutable v0.4.3 // indirect
-	github.com/bits-and-blooms/bitset v1.13.0 // indirect
+	github.com/benbjohnson/immutable v0.4.1-0.20221220213129-8932b999621d // indirect
+	github.com/bits-and-blooms/bitset v1.2.2 // indirect
 	github.com/bodgit/plumbing v1.3.0 // indirect
 	github.com/bodgit/windows v1.0.1 // indirect
 	github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
-	github.com/bytedance/sonic v1.10.2 // indirect
+	github.com/bytedance/sonic v1.9.1 // indirect
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
-	github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
-	github.com/chenzhuoyu/iasm v0.9.1 // indirect
-	github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
+	github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
+	github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/dgraph-io/badger v1.6.2 // indirect
+	github.com/dgraph-io/badger v1.6.0 // indirect
 	github.com/dgraph-io/ristretto v0.1.1 // indirect
-	github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
-	github.com/dustin/go-humanize v1.0.1 // indirect
+	github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 // indirect
+	github.com/dustin/go-humanize v1.0.0 // indirect
 	github.com/edsrzf/mmap-go v1.1.0 // indirect
 	github.com/fatih/structs v1.1.0 // indirect
-	github.com/fsnotify/fsnotify v1.7.0 // indirect
-	github.com/gabriel-vasile/mimetype v1.4.3 // indirect
+	github.com/fsnotify/fsnotify v1.6.0 // indirect
+	github.com/gabriel-vasile/mimetype v1.4.2 // indirect
 	github.com/gin-contrib/sse v0.1.0 // indirect
-	github.com/go-llsqlite/adapter v0.1.0 // indirect
-	github.com/go-llsqlite/crawshaw v0.5.0 // indirect
-	github.com/go-logr/logr v1.4.1 // indirect
+	github.com/go-llsqlite/adapter v0.0.0-20230927005056-7f5ce7f0c916 // indirect
+	github.com/go-llsqlite/crawshaw v0.4.0 // indirect
+	github.com/go-logr/logr v1.2.3 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-playground/locales v0.14.1 // indirect
 	github.com/go-playground/universal-translator v0.18.1 // indirect
-	github.com/go-playground/validator/v10 v10.16.0 // indirect
+	github.com/go-playground/validator/v10 v10.14.0 // indirect
 	github.com/goccy/go-json v0.10.2 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
-	github.com/golang/glog v1.2.0 // indirect
-	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
-	github.com/golang/protobuf v1.5.3 // indirect
-	github.com/golang/snappy v0.0.4 // indirect
+	github.com/golang/glog v1.0.0 // indirect
+	github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
+	github.com/golang/protobuf v1.5.2 // indirect
+	github.com/golang/snappy v0.0.3 // indirect
 	github.com/google/btree v1.1.2 // indirect
-	github.com/google/flatbuffers v23.5.26+incompatible // indirect
+	github.com/google/flatbuffers v1.12.1 // indirect
 	github.com/google/uuid v1.5.0 // indirect
-	github.com/gorilla/websocket v1.5.1 // indirect
-	github.com/hashicorp/errwrap v1.1.0 // indirect
+	github.com/gorilla/websocket v1.5.0 // indirect
+	github.com/hashicorp/errwrap v1.0.0 // indirect
 	github.com/hashicorp/go-multierror v1.1.1 // indirect
 	github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
-	github.com/huandu/xstrings v1.4.0 // indirect
+	github.com/huandu/xstrings v1.3.2 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/klauspost/compress v1.17.4 // indirect
-	github.com/klauspost/cpuid/v2 v2.2.6 // indirect
+	github.com/klauspost/cpuid/v2 v2.2.4 // indirect
 	github.com/knadh/koanf/maps v0.1.1 // indirect
 	github.com/leodido/go-urn v1.2.4 // indirect
-	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/mattn/go-isatty v0.0.19 // indirect
 	github.com/mitchellh/copystructure v1.2.0 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/mitchellh/reflectwalk v1.0.2 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/mschoch/smat v0.2.0 // indirect
-	github.com/pelletier/go-toml/v2 v2.1.1 // indirect
-	github.com/philippgille/gokv/util v0.6.0 // indirect
+	github.com/pelletier/go-toml/v2 v2.0.8 // indirect
+	github.com/philippgille/gokv/util v0.0.0-20191011213304-eb77f15b9c61 // indirect
 	github.com/pierrec/lz4/v4 v4.1.19 // indirect
-	github.com/pion/datachannel v1.5.5 // indirect
-	github.com/pion/dtls/v2 v2.2.8 // indirect
-	github.com/pion/ice/v2 v2.3.11 // indirect
-	github.com/pion/interceptor v0.1.25 // indirect
+	github.com/pion/datachannel v1.5.2 // indirect
+	github.com/pion/dtls/v2 v2.2.4 // indirect
+	github.com/pion/ice/v2 v2.2.6 // indirect
+	github.com/pion/interceptor v0.1.11 // indirect
 	github.com/pion/logging v0.2.2 // indirect
-	github.com/pion/mdns v0.0.9 // indirect
+	github.com/pion/mdns v0.0.5 // indirect
 	github.com/pion/randutil v0.1.0 // indirect
-	github.com/pion/rtcp v1.2.13 // indirect
-	github.com/pion/rtp v1.8.3 // indirect
-	github.com/pion/sctp v1.8.9 // indirect
-	github.com/pion/sdp/v3 v3.0.6 // indirect
-	github.com/pion/srtp/v2 v2.0.18 // indirect
-	github.com/pion/stun v0.6.1 // indirect
-	github.com/pion/transport/v2 v2.2.4 // indirect
-	github.com/pion/turn/v2 v2.1.4 // indirect
-	github.com/pion/webrtc/v3 v3.2.24 // indirect
+	github.com/pion/rtcp v1.2.9 // indirect
+	github.com/pion/rtp v1.7.13 // indirect
+	github.com/pion/sctp v1.8.2 // indirect
+	github.com/pion/sdp/v3 v3.0.5 // indirect
+	github.com/pion/srtp/v2 v2.0.9 // indirect
+	github.com/pion/stun v0.3.5 // indirect
+	github.com/pion/transport v0.13.1 // indirect
+	github.com/pion/transport/v2 v2.0.0 // indirect
+	github.com/pion/turn/v2 v2.0.8 // indirect
+	github.com/pion/udp v0.1.4 // indirect
+	github.com/pion/webrtc/v3 v3.1.42 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93 // indirect
 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
-	github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 // indirect
+	github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect
-	github.com/tidwall/btree v1.7.0 // indirect
+	github.com/tidwall/btree v1.6.0 // indirect
 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
-	github.com/ugorji/go/codec v1.2.12 // indirect
+	github.com/ugorji/go/codec v1.2.11 // indirect
 	github.com/ulikunitz/xz v0.5.11 // indirect
-	github.com/willscott/go-nfs-client v0.0.0-20200605172546-271fa9065b33 // indirect
-	github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect
-	go.etcd.io/bbolt v1.3.8 // indirect
-	go.opencensus.io v0.24.0 // indirect
-	go.opentelemetry.io/otel v1.21.0 // indirect
-	go.opentelemetry.io/otel/metric v1.21.0 // indirect
-	go.opentelemetry.io/otel/trace v1.21.0 // indirect
-	go4.org v0.0.0-20230225012048-214862532bf5 // indirect
-	golang.org/x/arch v0.6.0 // indirect
-	golang.org/x/crypto v0.17.0 // indirect
-	golang.org/x/sync v0.5.0 // indirect
-	golang.org/x/sys v0.15.0 // indirect
+	github.com/willscott/go-nfs-client v0.0.0-20240104095149-b44639837b00 // indirect
+	github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
+	go.etcd.io/bbolt v1.3.6 // indirect
+	go.opencensus.io v0.22.5 // indirect
+	go.opentelemetry.io/otel v1.8.0 // indirect
+	go.opentelemetry.io/otel/trace v1.8.0 // indirect
+	go4.org v0.0.0-20200411211856-f5505b9728dd // indirect
+	golang.org/x/arch v0.3.0 // indirect
+	golang.org/x/crypto v0.16.0 // indirect
+	golang.org/x/sync v0.3.0 // indirect
+	golang.org/x/sys v0.16.0 // indirect
 	golang.org/x/text v0.14.0 // indirect
-	golang.org/x/time v0.5.0 // indirect
-	google.golang.org/protobuf v1.32.0 // indirect
+	golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
+	google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	modernc.org/libc v1.38.0 // indirect
-	modernc.org/mathutil v1.6.0 // indirect
-	modernc.org/memory v1.7.2 // indirect
-	modernc.org/sqlite v1.28.0 // indirect
-	zombiezen.com/go/sqlite v1.0.0 // indirect
+	modernc.org/libc v1.22.3 // indirect
+	modernc.org/mathutil v1.5.0 // indirect
+	modernc.org/memory v1.5.0 // indirect
+	modernc.org/sqlite v1.21.1 // indirect
+	zombiezen.com/go/sqlite v0.13.1 // indirect
 )
diff --git a/go.sum b/go.sum
index c83405c..5bff810 100644
--- a/go.sum
+++ b/go.sum
@@ -24,12 +24,11 @@ github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIo
 github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
-github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w=
 github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI=
 github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
-github.com/RoaringBitmap/roaring v1.7.0 h1:OZF303tJCER1Tj3x+aArx/S5X7hrT186ri6JjrGvG68=
-github.com/RoaringBitmap/roaring v1.7.0/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
+github.com/RoaringBitmap/roaring v1.2.3 h1:yqreLINqIrX22ErkKI0vY47/ivtJr6n+kMhVOVmhWBY=
+github.com/RoaringBitmap/roaring v1.2.3/go.mod h1:plvDsJQpxOC5bw8LRteu/MLWHsHez/3y6cubLI4/1yE=
 github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
 github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
 github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 h1:byYvvbfSo3+9efR4IeReh77gVs4PnNDR3AMOE9NJ7a0=
@@ -53,15 +52,14 @@ github.com/anacrolix/envpprof v1.0.0/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54g
 github.com/anacrolix/envpprof v1.1.0/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4=
 github.com/anacrolix/envpprof v1.3.0 h1:WJt9bpuT7A/CDCxPOv/eeZqHWlle/Y0keJUvc6tcJDk=
 github.com/anacrolix/envpprof v1.3.0/go.mod h1:7QIG4CaX1uexQ3tqd5+BRa/9e2D02Wcertl6Yh0jCB0=
-github.com/anacrolix/generics v0.0.0-20230113004304-d6428d516633/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8=
-github.com/anacrolix/generics v0.0.0-20230911070922-5dd7545c6b13 h1:qwOprPTDMM3BASJRf84mmZnTXRsPGGJ8xoHKQS7m3so=
-github.com/anacrolix/generics v0.0.0-20230911070922-5dd7545c6b13/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8=
+github.com/anacrolix/generics v0.0.0-20230816105729-c755655aee45 h1:Kmcl3I9K2+5AdnnR7hvrnVT0TLeFWWMa9bxnm55aVIg=
+github.com/anacrolix/generics v0.0.0-20230816105729-c755655aee45/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8=
 github.com/anacrolix/go-libutp v1.3.1 h1:idJzreNLl+hNjGC3ZnUOjujEaryeOGgkwHLqSGoige0=
 github.com/anacrolix/go-libutp v1.3.1/go.mod h1:heF41EC8kN0qCLMokLBVkB8NXiLwx3t8R8810MTNI5o=
 github.com/anacrolix/log v0.3.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU=
 github.com/anacrolix/log v0.6.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU=
+github.com/anacrolix/log v0.10.1-0.20220123034749-3920702c17f8/go.mod h1:GmnE2c0nvz8pOIPUSC9Rawgefy1sDXqposC2wgtBZE4=
 github.com/anacrolix/log v0.13.1/go.mod h1:D4+CvN8SnruK6zIFS/xPoRJmtvtnxs+CSfDQ+BFxZ68=
-github.com/anacrolix/log v0.14.2/go.mod h1:1OmJESOtxQGNMlUO5rcv96Vpp9mfMqXXbe2RdinFLdY=
 github.com/anacrolix/log v0.14.5 h1:OkMjBquVSRb742LkecSGDGaGpNoSrw4syRIm0eRdmrg=
 github.com/anacrolix/log v0.14.5/go.mod h1:1OmJESOtxQGNMlUO5rcv96Vpp9mfMqXXbe2RdinFLdY=
 github.com/anacrolix/lsan v0.0.0-20211126052245-807000409a62 h1:P04VG6Td13FHMgS5ZBcJX23NPC/fiC4cp9bXwYujdYM=
@@ -76,16 +74,17 @@ github.com/anacrolix/missinggo/perf v1.0.0 h1:7ZOGYziGEBytW49+KmYGTaNfnwUqP1HBsy
 github.com/anacrolix/missinggo/perf v1.0.0/go.mod h1:ljAFWkBuzkO12MQclXzZrosP5urunoLS0Cbvb4V0uMQ=
 github.com/anacrolix/missinggo/v2 v2.2.0/go.mod h1:o0jgJoYOyaoYQ4E2ZMISVa9c88BbUBVQQW4QeRkNCGY=
 github.com/anacrolix/missinggo/v2 v2.5.1/go.mod h1:WEjqh2rmKECd0t1VhQkLGTdIWXO6f6NLjp5GlMZ+6FA=
+github.com/anacrolix/missinggo/v2 v2.5.2/go.mod h1:yNvsLrtZYRYCOI+KRH/JM8TodHjtIE/bjOGhQaLOWIE=
 github.com/anacrolix/missinggo/v2 v2.7.3 h1:Ee//CmZBMadeNiYB/hHo9ly2PFOEZ4Fhsbnug3rDAIE=
 github.com/anacrolix/missinggo/v2 v2.7.3/go.mod h1:mIEtp9pgaXqt8VQ3NQxFOod/eQ1H0D1XsZzKUQfwtac=
 github.com/anacrolix/mmsg v0.0.0-20180515031531-a4a3ba1fc8bb/go.mod h1:x2/ErsYUmT77kezS63+wzZp8E3byYB0gzirM/WMBLfw=
 github.com/anacrolix/mmsg v1.0.0 h1:btC7YLjOn29aTUAExJiVUhQOuf/8rhm+/nWCMAnL3Hg=
 github.com/anacrolix/mmsg v1.0.0/go.mod h1:x8kRaJY/dCrY9Al0PEcj1mb/uFHwP6GCJ9fLl4thEPc=
-github.com/anacrolix/multiless v0.3.1-0.20230203023154-f3d27407d8f1 h1:1gfWAUiwUurVDZ4Re9e1hhpF0iGLlVBhPL5DY5U5hrI=
-github.com/anacrolix/multiless v0.3.1-0.20230203023154-f3d27407d8f1/go.mod h1:zJv1JF9AqdZiHwxqPgjuOZDGWER6nyE48WBCi/OOrMM=
+github.com/anacrolix/multiless v0.3.1-0.20221221005021-2d12701f83f7 h1:lOtCD+LzoD1g7bowhYJNR++uV+FyY5bTZXKwnPex9S8=
+github.com/anacrolix/multiless v0.3.1-0.20221221005021-2d12701f83f7/go.mod h1:zJv1JF9AqdZiHwxqPgjuOZDGWER6nyE48WBCi/OOrMM=
 github.com/anacrolix/stm v0.2.0/go.mod h1:zoVQRvSiGjGoTmbM0vSLIiaKjWtNPeTvXUSdJQA4hsg=
-github.com/anacrolix/stm v0.5.0 h1:9df1KBpttF0TzLgDq51Z+TEabZKMythqgx89f1FQJt8=
-github.com/anacrolix/stm v0.5.0/go.mod h1:MOwrSy+jCm8Y7HYfMAwPj7qWVu7XoVvjOiYwJmpeB/M=
+github.com/anacrolix/stm v0.4.1-0.20221221005312-96d17df0e496 h1:aMiRi2kOOd+nG64suAmFMVnNK2E6GsnLif7ia9tI3cA=
+github.com/anacrolix/stm v0.4.1-0.20221221005312-96d17df0e496/go.mod h1:DBm8/1OXm4A4RZ6Xa9u/eOsjeAXCaoRYvd2JzlskXeM=
 github.com/anacrolix/sync v0.0.0-20180808010631-44578de4e778/go.mod h1:s735Etp3joe/voe2sdaXLcqDdJSay1O0OPnM0ystjqk=
 github.com/anacrolix/sync v0.3.0/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DCOj/I0g=
 github.com/anacrolix/sync v0.5.1 h1:FbGju6GqSjzVoTgcXTUKkF041lnZkG5P0C3T5RL3SGc=
@@ -95,10 +94,10 @@ github.com/anacrolix/tagflag v1.0.0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pm
 github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8=
 github.com/anacrolix/torrent v1.53.2 h1:dW+ficSC8sJaGrUvZJizORPBLTP7XR8idl2oGlrUutQ=
 github.com/anacrolix/torrent v1.53.2/go.mod h1:d1NANCFAd9/nv9vmHnYUobLdyBSAoFYohojHjGmcAsw=
-github.com/anacrolix/upnp v0.1.3 h1:NlYEhE75adz2npEJKjbqyqnyW9qU4STookvSNXBJ5ao=
-github.com/anacrolix/upnp v0.1.3/go.mod h1:Qyhbqo69gwNWvEk1xNTXsS5j7hMHef9hdr984+9fIic=
-github.com/anacrolix/utp v0.2.0 h1:65Cdmr6q9WSw2KsM+rtJFu7rqDzLl2bdysf4KlNPcFI=
-github.com/anacrolix/utp v0.2.0/go.mod h1:HGk4GYQw1O/3T1+yhqT/F6EcBd+AAwlo9dYErNy7mj8=
+github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96 h1:QAVZ3pN/J4/UziniAhJR2OZ9Ox5kOY2053tBbbqUPYA=
+github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96/go.mod h1:Wa6n8cYIdaG35x15aH3Zy6d03f7P728QfdcDeD/IEOs=
+github.com/anacrolix/utp v0.1.0 h1:FOpQOmIwYsnENnz7tAGohA+r6iXpRjrq8ssKSre2Cp4=
+github.com/anacrolix/utp v0.1.0/go.mod h1:MDwc+vsGEq7RMw6lr2GKOEqjWny5hO5OZXRVNaBJ2Dk=
 github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
 github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
 github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
@@ -106,16 +105,16 @@ github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5
 github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
 github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
 github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI=
-github.com/benbjohnson/immutable v0.4.3 h1:GYHcksoJ9K6HyAUpGxwZURrbTkXA0Dh4otXGqbhdrjA=
-github.com/benbjohnson/immutable v0.4.3/go.mod h1:qJIKKSmdqz1tVzNtst1DZzvaqOU1onk1rc03IeM3Owk=
+github.com/benbjohnson/immutable v0.4.1-0.20221220213129-8932b999621d h1:2qVb9bsAMtmAfnxXltm+6eBzrrS7SZ52c3SedsulaMI=
+github.com/benbjohnson/immutable v0.4.1-0.20221220213129-8932b999621d/go.mod h1:iAr8OjJGLnLmVUr9MZ/rz4PWUy6Ouc2JLYuMArmvAJM=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/billziss-gh/cgofuse v1.5.0 h1:kH516I/s+Ab4diL/Y/ayFeUjjA8ey+JK12xDfBf4HEs=
 github.com/billziss-gh/cgofuse v1.5.0/go.mod h1:LJjoaUojlVjgo5GQoEJTcJNqZJeRU0nCR84CyxKt2YM=
-github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
-github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
-github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
+github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
+github.com/bits-and-blooms/bitset v1.2.2 h1:J5gbX05GpMdBjCvQ9MteIg2KKDExr7DrgK+Yc15FvIk=
+github.com/bits-and-blooms/bitset v1.2.2/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
 github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
 github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
 github.com/bodgit/sevenzip v1.4.5 h1:HFJQ+nbjppfyf2xbQEJBbmVo+o2kTg1FXV4i7YOx87s=
@@ -127,74 +126,60 @@ github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2w
 github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8=
 github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8/go.mod h1:spo1JLcs67NmW1aVLEgtA8Yy1elc+X8y5SRW1sFW4Og=
 github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
-github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
-github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=
-github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
+github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
+github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
-github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
 github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
+github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
 github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
-github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
-github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
-github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
-github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
-github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
-github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
 github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
 github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
-github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
-github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
+github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgraph-io/badger v1.6.0 h1:DshxFxZWXUcO0xX476VJC07Xsr6ZCBVRHKZ93Oh7Evo=
 github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
-github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8=
-github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE=
 github.com/dgraph-io/badger/v4 v4.2.0 h1:kJrlajbXXL9DFTNuhhu9yCx7JJa4qpYWxtE8BzuWsEs=
 github.com/dgraph-io/badger/v4 v4.2.0/go.mod h1:qfCqhPoWDFJRx1gp5QwwyGo8xk1lbHUxvK9nK0OGAak=
-github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
 github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
 github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
+github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
 github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
-github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
-github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
 github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
 github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
-github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
-github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
 github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
 github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
 github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ=
 github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
-github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
 github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
 github.com/frankban/quicktest v1.9.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
-github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
 github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
 github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
-github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
-github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
-github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
-github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
+github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
+github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
+github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
+github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
 github.com/gin-contrib/pprof v1.4.0 h1:XxiBSf5jWZ5i16lNOPbMTVdgHBdhfGRD5PZ1LWazzvg=
 github.com/gin-contrib/pprof v1.4.0/go.mod h1:RrehPJasUVBPK6yTUwOl8/NP6i0vbUgmxtis+Z5KE90=
 github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
@@ -214,15 +199,15 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-llsqlite/adapter v0.1.0 h1:wGSQNsu/rtYeu/lqZNZQMjwUdEF3OW66xTLvsFwJQUw=
-github.com/go-llsqlite/adapter v0.1.0/go.mod h1:DADrR88ONKPPeSGjFp5iEN55Arx3fi2qXZeKCYDpbmU=
-github.com/go-llsqlite/crawshaw v0.5.0 h1:Olbqkth53vkkh4WvmkYjrtfOBcxXD3rMYBYuk6FNH3E=
-github.com/go-llsqlite/crawshaw v0.5.0/go.mod h1:/YJdV7uBQaYDE0fwe4z3wwJIZBJxdYzd38ICggWqtaE=
+github.com/go-llsqlite/adapter v0.0.0-20230927005056-7f5ce7f0c916 h1:OyQmpAN302wAopDgwVjgs2HkFawP9ahIEqkUYz7V7CA=
+github.com/go-llsqlite/adapter v0.0.0-20230927005056-7f5ce7f0c916/go.mod h1:DADrR88ONKPPeSGjFp5iEN55Arx3fi2qXZeKCYDpbmU=
+github.com/go-llsqlite/crawshaw v0.4.0 h1:L02s2jZBBJj80xm1VkkdyB/JlQ/Fi0kLbNHfXA8yrec=
+github.com/go-llsqlite/crawshaw v0.4.0/go.mod h1:/YJdV7uBQaYDE0fwe4z3wwJIZBJxdYzd38ICggWqtaE=
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
-github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
+github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
@@ -235,8 +220,8 @@ github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl
 github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
 github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
-github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE=
-github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
+github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
+github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
 github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
@@ -250,13 +235,12 @@ github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68=
-github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
+github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
+github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
 github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
@@ -270,42 +254,35 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x
 github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
 github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
 github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
-github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
 github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
-github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
-github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
+github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
 github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
-github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg=
-github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
+github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw=
+github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
-github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
 github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
@@ -316,11 +293,10 @@ github.com/gopherjs/gopherjs v0.0.0-20190309154008-847fc94819f9/go.mod h1:wJfORR
 github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
 github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
-github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
-github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
+github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
+github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
-github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
-github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
 github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@@ -335,8 +311,8 @@ github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbc
 github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
 github.com/huandu/xstrings v1.3.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
 github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
-github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
-github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
+github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
+github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
@@ -353,8 +329,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
 github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
 github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
 github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
-github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
-github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
+github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
+github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
 github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs=
 github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
 github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w=
@@ -367,7 +343,6 @@ github.com/knadh/koanf/providers/structs v0.1.0 h1:wJRteCNn1qvLtE5h8KQBvLJovidSd
 github.com/knadh/koanf/providers/structs v0.1.0/go.mod h1:sw2YZ3txUcqA3Z27gPlmmBzWn1h8Nt9O6EP/91MkcWE=
 github.com/knadh/koanf/v2 v2.0.1 h1:1dYGITt1I23x8cfx8ZnldtezdyaZtfAuRtIFOiRzK7g=
 github.com/knadh/koanf/v2 v2.0.1/go.mod h1:ZeiIlIDXTE7w1lMT6UVcNiRAS2/rCeLn/GdLNvY1Dus=
-github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -388,9 +363,8 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec
 github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
 github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
-github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
@@ -427,8 +401,8 @@ github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAl
 github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
-github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
-github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
+github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
+github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
 github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
 github.com/philippgille/gokv v0.0.0-20191001201555-5ac9a20de634/go.mod h1:OCoWPt+mbYuTO1FUVrQ2SxQU0oaaHBsn6lRhFX3JHOc=
 github.com/philippgille/gokv v0.5.1-0.20191011213304-eb77f15b9c61/go.mod h1:OCoWPt+mbYuTO1FUVrQ2SxQU0oaaHBsn6lRhFX3JHOc=
@@ -441,59 +415,54 @@ github.com/philippgille/gokv/encoding v0.6.0 h1:P1TN+Aulpd6Qd7qcLqgPwoxzOQ42UHBX
 github.com/philippgille/gokv/encoding v0.6.0/go.mod h1:/yKvq2BKJlKJsH7KMDrhDlEw2Pt3V1nKyFhs4iOqz5U=
 github.com/philippgille/gokv/test v0.0.0-20191011213304-eb77f15b9c61 h1:4tVyBgfpK0NSqu7tNZTwYfC/pbyWUR2y+O7mxEg5BTQ=
 github.com/philippgille/gokv/test v0.0.0-20191011213304-eb77f15b9c61/go.mod h1:EUc+s9ONc1+VOr9NUEd8S0YbGRrQd/gz/p+2tvwt12s=
+github.com/philippgille/gokv/util v0.0.0-20191011213304-eb77f15b9c61 h1:ril/jI0JgXNjPWwDkvcRxlZ09kgHXV2349xChjbsQ4o=
 github.com/philippgille/gokv/util v0.0.0-20191011213304-eb77f15b9c61/go.mod h1:2dBhsJgY/yVIkjY5V3AnDUxUbEPzT6uQ3LvoVT8TR20=
-github.com/philippgille/gokv/util v0.6.0 h1:GrTxVENzKBxs8lB3tnaA88mKOuVPT7atZPplxX+PNmo=
-github.com/philippgille/gokv/util v0.6.0/go.mod h1:ovoDHZ2Svr7YX972SPPJQRXbhHEy3Gb20HRH/Tr9BiQ=
 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
 github.com/pierrec/lz4/v4 v4.1.19 h1:tYLzDnjDXh9qIxSTKHwXwOYmm9d887Y7Y1ZkyXYHAN4=
 github.com/pierrec/lz4/v4 v4.1.19/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
-github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8=
-github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
-github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
-github.com/pion/dtls/v2 v2.2.8 h1:BUroldfiIbV9jSnC6cKOMnyiORRWrWWpV11JUyEu5OA=
-github.com/pion/dtls/v2 v2.2.8/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
-github.com/pion/ice/v2 v2.3.11 h1:rZjVmUwyT55cmN8ySMpL7rsS8KYsJERsrxJLLxpKhdw=
-github.com/pion/ice/v2 v2.3.11/go.mod h1:hPcLC3kxMa+JGRzMHqQzjoSj3xtE9F+eoncmXLlCL4E=
-github.com/pion/interceptor v0.1.25 h1:pwY9r7P6ToQ3+IF0bajN0xmk/fNw/suTgaTdlwTDmhc=
-github.com/pion/interceptor v0.1.25/go.mod h1:wkbPYAak5zKsfpVDYMtEfWEy8D4zL+rpxCxPImLOg3Y=
+github.com/pion/datachannel v1.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E=
+github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ=
+github.com/pion/dtls/v2 v2.1.3/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
+github.com/pion/dtls/v2 v2.1.5/go.mod h1:BqCE7xPZbPSubGasRoDFJeTsyJtdD1FanJYL0JGheqY=
+github.com/pion/dtls/v2 v2.2.4 h1:YSfYwDQgrxMYXLBc/m7PFY5BVtWlNm/DN4qoU2CbcWg=
+github.com/pion/dtls/v2 v2.2.4/go.mod h1:WGKfxqhrddne4Kg3p11FUMJrynkOY4lb25zHNO49wuw=
+github.com/pion/ice/v2 v2.2.6 h1:R/vaLlI1J2gCx141L5PEwtuGAGcyS6e7E0hDeJFq5Ig=
+github.com/pion/ice/v2 v2.2.6/go.mod h1:SWuHiOGP17lGromHTFadUe1EuPgFh/oCU6FCMZHooVE=
+github.com/pion/interceptor v0.1.11 h1:00U6OlqxA3FFB50HSg25J/8cWi7P6FbSzw4eFn24Bvs=
+github.com/pion/interceptor v0.1.11/go.mod h1:tbtKjZY14awXd7Bq0mmWvgtHB5MDaRN7HV3OZ/uy7s8=
 github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
 github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
-github.com/pion/mdns v0.0.8/go.mod h1:hYE72WX8WDveIhg7fmXgMKivD3Puklk0Ymzog0lSyaI=
-github.com/pion/mdns v0.0.9 h1:7Ue5KZsqq8EuqStnpPWV33vYYEH0+skdDN5L7EiEsI4=
-github.com/pion/mdns v0.0.9/go.mod h1:2JA5exfxwzXiCihmxpTKgFUpiQws2MnipoPK09vecIc=
+github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw=
+github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g=
 github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
 github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
-github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
-github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
-github.com/pion/rtcp v1.2.13 h1:+EQijuisKwm/8VBs8nWllr0bIndR7Lf7cZG200mpbNo=
-github.com/pion/rtcp v1.2.13/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
-github.com/pion/rtp v1.8.2/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
-github.com/pion/rtp v1.8.3 h1:VEHxqzSVQxCkKDSHro5/4IUUG1ea+MFdqR2R3xSpNU8=
-github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
-github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
-github.com/pion/sctp v1.8.8/go.mod h1:igF9nZBrjh5AtmKc7U30jXltsFHicFCXSmWA2GWRaWs=
-github.com/pion/sctp v1.8.9 h1:TP5ZVxV5J7rz7uZmbyvnUvsn7EJ2x/5q9uhsTtXbI3g=
-github.com/pion/sctp v1.8.9/go.mod h1:cMLT45jqw3+jiJCrtHVwfQLnfR0MGZ4rgOJwUOIqLkI=
-github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw=
-github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
-github.com/pion/srtp/v2 v2.0.18 h1:vKpAXfawO9RtTRKZJbG4y0v1b11NZxQnxRl85kGuUlo=
-github.com/pion/srtp/v2 v2.0.18/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA=
-github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
-github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
-github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
-github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
-github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
-github.com/pion/transport/v2 v2.2.2/go.mod h1:OJg3ojoBJopjEeECq2yJdXH9YVrUJ1uQ++NjXLOUorc=
-github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
-github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo=
-github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
-github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM=
-github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
-github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
-github.com/pion/turn/v2 v2.1.4 h1:2xn8rduI5W6sCZQkEnIUDAkrBQNl2eYIBCHMZ3QMmP8=
-github.com/pion/turn/v2 v2.1.4/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
-github.com/pion/webrtc/v3 v3.2.24 h1:MiFL5DMo2bDaaIFWr0DDpwiV/L4EGbLZb+xoRvfEo1Y=
-github.com/pion/webrtc/v3 v3.2.24/go.mod h1:1CaT2fcZzZ6VZA+O1i9yK2DU4EOcXVvSbWG9pr5jefs=
+github.com/pion/rtcp v1.2.9 h1:1ujStwg++IOLIEoOiIQ2s+qBuJ1VN81KW+9pMPsif+U=
+github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo=
+github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA=
+github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
+github.com/pion/sctp v1.8.0/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
+github.com/pion/sctp v1.8.2 h1:yBBCIrUMJ4yFICL3RIvR4eh/H2BTTvlligmSTy+3kiA=
+github.com/pion/sctp v1.8.2/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
+github.com/pion/sdp/v3 v3.0.5 h1:ouvI7IgGl+V4CrqskVtr3AaTrPvPisEOxwgpdktctkU=
+github.com/pion/sdp/v3 v3.0.5/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
+github.com/pion/srtp/v2 v2.0.9 h1:JJq3jClmDFBPX/F5roEb0U19jSU7eUhyDqR/NZ34EKQ=
+github.com/pion/srtp/v2 v2.0.9/go.mod h1:5TtM9yw6lsH0ppNCehB/EjEUli7VkUgKSPJqWVqbhQ4=
+github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
+github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
+github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
+github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A=
+github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g=
+github.com/pion/transport v0.13.1 h1:/UH5yLeQtwm2VZIPjxwnNFxjS4DFhyLfS4GlfuKUzfA=
+github.com/pion/transport v0.13.1/go.mod h1:EBxbqzyv+ZrmDb82XswEE0BjfQFtuw1Nu6sjnjWCsGg=
+github.com/pion/transport/v2 v2.0.0 h1:bsMYyqHCbkvHwj+eNCFBuxtlKndKfyGI2vaQmM3fIE4=
+github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=
+github.com/pion/turn/v2 v2.0.8 h1:KEstL92OUN3k5k8qxsXHpr7WWfrdp7iJZHx99ud8muw=
+github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
+github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
+github.com/pion/udp v0.1.4 h1:OowsTmu1Od3sD6i3fQUJxJn2fEvJO6L1TidgadtbTI8=
+github.com/pion/udp v0.1.4/go.mod h1:G8LDo56HsFwC24LIcnT4YIDU5qcB6NepqqjP0keL2us=
+github.com/pion/webrtc/v3 v3.1.42 h1:wJEQFIXVanptnQcHOLTuIo4AtGB2+mG2x4OhIhnITOA=
+github.com/pion/webrtc/v3 v3.1.42/go.mod h1:ffD9DulDrPxyWvDPUIPAOSAWx9GUlOExiJPf7cCcMLA=
 github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -521,16 +490,16 @@ github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4
 github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93 h1:UVArwN/wkKjMVhh2EQGC0tEc1+FqiLlvYXY5mQ2f8Wg=
 github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93/go.mod h1:Nfe4efndBz4TibWycNE+lqyJZiMX4ycx+QKV8Ta0f/o=
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
+github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
 github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
-github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
 github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
 github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
-github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 h1:18kd+8ZUlt/ARXhljq+14TwAoKa61q6dX8jtwOf6DH8=
-github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA=
+github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 h1:Lt9DzQALzHoDwMBGJ6v8ObDPR0dzr2a6sXTB1Fq7IHs=
+github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA=
 github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
 github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
 github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
@@ -549,8 +518,6 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1
 github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
 github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs=
-github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
-github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
 github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
 github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
 github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
@@ -575,8 +542,8 @@ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
 github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI=
-github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
+github.com/tidwall/btree v1.6.0 h1:LDZfKfQIBHGHWSwckhXI0RPSXzlo+KYdjK7FWSqOzzg=
+github.com/tidwall/btree v1.6.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
 github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
 github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
 github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
@@ -585,47 +552,48 @@ github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2
 github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
 github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
 github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
-github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
-github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
+github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
 github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
 github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 github.com/urfave/cli/v2 v2.27.0 h1:uNs1K8JwTFL84X68j5Fjny6hfANh9nTlJ6dRtZAFAHY=
 github.com/urfave/cli/v2 v2.27.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
 github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
 github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
-github.com/willscott/go-nfs v0.0.1 h1:392gV283iuisKFeV9hkKwTdCRfizP+R9FC+gYg2skj0=
-github.com/willscott/go-nfs v0.0.1/go.mod h1:hBPyqKNde3v8rzxDVWtloP6MtLnx/7aVz3XxxP89W7k=
+github.com/willscott/go-nfs v0.0.2-0.20240102104133-9c599ee601d3 h1:ObGKsnV2OfjSlp0sCJwV0toiQl2eoqlFwrln1W5aBPg=
+github.com/willscott/go-nfs v0.0.2-0.20240102104133-9c599ee601d3/go.mod h1:+7+CzZfrWAP2Ff9h/6MhCMrjmitC21Yxt7nF/erAHNM=
+github.com/willscott/go-nfs v0.0.2 h1:BaBp1CpGDMooCT6bCgX6h6ZwgPcTMST4yToYZ9byee0=
+github.com/willscott/go-nfs v0.0.2/go.mod h1:SvullWeHxr/924WQNbUaZqtluBt2vuZ61g6yAV+xj7w=
 github.com/willscott/go-nfs-client v0.0.0-20200605172546-271fa9065b33 h1:Wd8wdpRzPXskyHvZLyw7Wc1fp5oCE2mhBCj7bAiibUs=
 github.com/willscott/go-nfs-client v0.0.0-20200605172546-271fa9065b33/go.mod h1:cOUKSNty+RabZqKhm5yTJT5Vq/Fe83ZRWAJ5Kj8nRes=
+github.com/willscott/go-nfs-client v0.0.0-20240104095149-b44639837b00 h1:U0DnHRZFzoIV1oFEZczg5XyPut9yxk9jjtax/9Bxr/o=
+github.com/willscott/go-nfs-client v0.0.0-20240104095149-b44639837b00/go.mod h1:Tq++Lr/FgiS3X48q5FETemXiSLGuYMQT2sPjYNPJSwA=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
-github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI=
-github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
+github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
+github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 github.com/zema1/go-nfs-client v0.0.0-20200604081958-0cf942f0e0fe/go.mod h1:im3CVJ32XM3+E+2RhY0sa5IVJVQehUrX0oE1wX4xOwU=
-go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
-go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
+go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
+go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
 go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
 go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
 go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
 go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
 go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
-go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
-go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
-go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
-go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
-go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
-go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
-go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
-go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
-go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
+go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0=
+go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
+go.opentelemetry.io/otel v1.8.0 h1:zcvBFizPbpa1q7FehvFiHbQwGzmPILebO0tyqIR5Djg=
+go.opentelemetry.io/otel v1.8.0/go.mod h1:2pkj+iMj0o03Y+cW6/m8Y4WkRdYN3AvCXCnzRMp9yvM=
+go.opentelemetry.io/otel/trace v1.8.0 h1:cSy0DF9eGI5WIfNwZ1q2iUyGj00tGzP24dE1lOlHrfY=
+go.opentelemetry.io/otel/trace v1.8.0/go.mod h1:0Bt3PXY8w+3pheS3hQUt+wow8b1ojPaTBoTCh2zIFI4=
+go4.org v0.0.0-20200411211856-f5505b9728dd h1:BNJlw5kRTzdmyfh5U8F93HA2OwkP7ZGwA51eJ/0wKOU=
+go4.org v0.0.0-20200411211856-f5505b9728dd/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZMZXMSBGhxRdsvzbkg=
 golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
-golang.org/x/arch v0.6.0 h1:S0JTfE48HbRj80+4tbvZDYsJ3tGv6BUU3XxyZ7CirAc=
-golang.org/x/arch v0.6.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
+golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
+golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -635,13 +603,12 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
-golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
-golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
-golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
-golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
-golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
-golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
+golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
+golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -650,7 +617,6 @@ golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE
 golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
-golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
 golang.org/x/exp v0.0.0-20231226003508-02704c960a9b h1:kLiC65FbiHWFAOu+lxwNPujcsl8VYyTYYEZnsOO1WK4=
 golang.org/x/exp v0.0.0-20231226003508-02704c960a9b/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
@@ -671,10 +637,7 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
 golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
-golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -693,23 +656,24 @@ golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20191011234655-491137f69257/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
-golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220401154927-543a649e0bdd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
-golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
 golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
-golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
-golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
-golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
-golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
 golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
 golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -724,11 +688,9 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
-golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
+golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -754,8 +716,10 @@ golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -763,33 +727,29 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
 golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
+golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
-golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
-golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
-golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
-golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
-golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
-golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -798,18 +758,14 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
-golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U=
+golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -838,9 +794,7 @@ golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapK
 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM=
 golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -874,31 +828,29 @@ google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvx
 google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
 google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
 google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w=
+google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
 google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
 google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
+google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
 google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
 google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
-google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
-google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
+google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -926,18 +878,17 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
-modernc.org/libc v1.38.0 h1:o4Lpk0zNDSdsjfEXnF1FGXWQ9PDi1NOdWcLP5n13FGo=
-modernc.org/libc v1.38.0/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
-modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
-modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
-modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
-modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
-modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
-modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
-nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
+modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY=
+modernc.org/libc v1.22.3/go.mod h1:MQrloYP209xa2zHome2a8HLiLm6k0UT8CoHpV74tOFw=
+modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
+modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
+modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
+modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
+modernc.org/sqlite v1.21.1 h1:GyDFqNnESLOhwwDRaHGdp2jKLDzpyT/rNLglX3ZkMSU=
+modernc.org/sqlite v1.21.1/go.mod h1:XwQ0wZPIh1iKb5mkvCJ3szzbhk+tykC8ZWqTRTgYRwI=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
 rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
-zombiezen.com/go/sqlite v1.0.0 h1:D2EvOZqumJBy+6t+0uNTTXnepUpB/pKG45op/UziI1o=
-zombiezen.com/go/sqlite v1.0.0/go.mod h1:Yx7FJ77tr7Ucwi5solhXAxpflyxk/BHNXArZ/JvDm60=
+zombiezen.com/go/sqlite v0.13.1 h1:qDzxyWWmMtSSEH5qxamqBFmqA2BLSSbtODi3ojaE02o=
+zombiezen.com/go/sqlite v0.13.1/go.mod h1:Ht/5Rg3Ae2hoyh1I7gbWtWAl89CNocfqeb/aAMTkJr4=
diff --git a/src/config/default.go b/src/config/default.go
index 0d267b3..9a3ebcb 100644
--- a/src/config/default.go
+++ b/src/config/default.go
@@ -1,7 +1,7 @@
 package config
 
 var defaultConfig = Config{
-	DataFolder: "./data",
+	SourceDir: "./data",
 	WebUi: WebUi{
 		Port: 4444,
 		IP:   "0.0.0.0",
diff --git a/src/config/model.go b/src/config/model.go
index d967ae8..88ec4b4 100644
--- a/src/config/model.go
+++ b/src/config/model.go
@@ -7,7 +7,7 @@ type Config struct {
 	Mounts        Mounts        `koanf:"mounts"`
 	Log           Log           `koanf:"log"`
 
-	DataFolder string `koanf:"dataFolder"`
+	SourceDir string `koanf:"source_dir"`
 }
 
 type WebUi struct {
diff --git a/src/export/nfs/handler.go b/src/export/nfs/handler.go
index d340062..4c2acee 100644
--- a/src/export/nfs/handler.go
+++ b/src/export/nfs/handler.go
@@ -2,14 +2,22 @@ package nfs
 
 import (
 	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
+	"git.kmsign.ru/royalcat/tstor/src/log"
+	zlog "github.com/rs/zerolog/log"
 	nfs "github.com/willscott/go-nfs"
 	nfshelper "github.com/willscott/go-nfs/helpers"
 )
 
 func NewNFSv3Handler(fs vfs.Filesystem) (nfs.Handler, error) {
-	bfs := &billyFsWrapper{fs: fs}
+	nfslog := zlog.Logger.With().Str("component", "nfs").Logger()
+	nfs.SetLogger(log.NewNFSLog(nfslog))
+	nfs.Log.SetLevel(nfs.InfoLevel)
+
+	bfs := &billyFsWrapper{fs: fs, log: nfslog}
 	handler := nfshelper.NewNullAuthHandler(bfs)
+
 	cacheHelper := nfshelper.NewCachingHandler(handler, 1024*16)
+
 	//  cacheHelper := NewCachingHandler(handler)
 
 	return cacheHelper, nil
diff --git a/src/export/nfs/wrapper-v3.go b/src/export/nfs/wrapper-v3.go
index 195bd41..e855221 100644
--- a/src/export/nfs/wrapper-v3.go
+++ b/src/export/nfs/wrapper-v3.go
@@ -1,15 +1,18 @@
 package nfs
 
 import (
+	"errors"
 	"io/fs"
 	"path/filepath"
 
 	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
 	"github.com/go-git/go-billy/v5"
+	"github.com/rs/zerolog"
 )
 
 type billyFsWrapper struct {
-	fs vfs.Filesystem
+	fs  vfs.Filesystem
+	log zerolog.Logger
 }
 
 var _ billy.Filesystem = (*billyFsWrapper)(nil)
@@ -34,7 +37,7 @@ func (*billyFsWrapper) Join(elem ...string) string {
 func (fs *billyFsWrapper) Lstat(filename string) (fs.FileInfo, error) {
 	info, err := fs.fs.Stat(filename)
 	if err != nil {
-		return nil, billyErr(err)
+		return nil, billyErr(err, fs.log)
 	}
 	return info, nil
 }
@@ -45,26 +48,28 @@ func (*billyFsWrapper) MkdirAll(filename string, perm fs.FileMode) error {
 }
 
 // Open implements billy.Filesystem.
-func (f *billyFsWrapper) Open(filename string) (billy.File, error) {
-	file, err := f.fs.Open(filename)
+func (fs *billyFsWrapper) Open(filename string) (billy.File, error) {
+	file, err := fs.fs.Open(filename)
 	if err != nil {
-		return nil, billyErr(err)
+		return nil, billyErr(err, fs.log)
 	}
 	return &billyFile{
 		name: filename,
 		file: file,
+		log:  fs.log.With().Str("filename", filename).Logger(),
 	}, nil
 }
 
 // OpenFile implements billy.Filesystem.
-func (f *billyFsWrapper) OpenFile(filename string, flag int, perm fs.FileMode) (billy.File, error) {
-	file, err := f.fs.Open(filename)
+func (fs *billyFsWrapper) OpenFile(filename string, flag int, perm fs.FileMode) (billy.File, error) {
+	file, err := fs.fs.Open(filename)
 	if err != nil {
-		return nil, billyErr(err)
+		return nil, billyErr(err, fs.log)
 	}
 	return &billyFile{
 		name: filename,
 		file: file,
+		log:  fs.log.With().Str("filename", filename).Int("flag", flag).Str("perm", perm.String()).Logger(),
 	}, nil
 }
 
@@ -72,7 +77,7 @@ func (f *billyFsWrapper) OpenFile(filename string, flag int, perm fs.FileMode) (
 func (bfs *billyFsWrapper) ReadDir(path string) ([]fs.FileInfo, error) {
 	ffs, err := bfs.fs.ReadDir(path)
 	if err != nil {
-		return nil, billyErr(err)
+		return nil, billyErr(err, bfs.log)
 	}
 
 	out := make([]fs.FileInfo, 0, len(ffs))
@@ -112,27 +117,28 @@ func (*billyFsWrapper) Root() string {
 }
 
 // Stat implements billy.Filesystem.
-func (f *billyFsWrapper) Stat(filename string) (fs.FileInfo, error) {
-	info, err := f.fs.Stat(filename)
+func (fs *billyFsWrapper) Stat(filename string) (fs.FileInfo, error) {
+	info, err := fs.fs.Stat(filename)
 	if err != nil {
-		return nil, billyErr(err)
+		return nil, billyErr(err, fs.log)
 	}
 	return info, nil
 }
 
 // Symlink implements billy.Filesystem.
-func (*billyFsWrapper) Symlink(target string, link string) error {
-	return billyErr(vfs.ErrNotImplemented)
+func (fs *billyFsWrapper) Symlink(target string, link string) error {
+	return billyErr(vfs.ErrNotImplemented, fs.log)
 }
 
 // TempFile implements billy.Filesystem.
-func (*billyFsWrapper) TempFile(dir string, prefix string) (billy.File, error) {
-	return nil, billyErr(vfs.ErrNotImplemented)
+func (fs *billyFsWrapper) TempFile(dir string, prefix string) (billy.File, error) {
+	return nil, billyErr(vfs.ErrNotImplemented, fs.log)
 }
 
 type billyFile struct {
 	name string
 	file vfs.File
+	log  zerolog.Logger
 }
 
 var _ billy.File = (*billyFile)(nil)
@@ -149,27 +155,27 @@ func (f *billyFile) Name() string {
 
 // Read implements billy.File.
 func (f *billyFile) Read(p []byte) (n int, err error) {
-	return f.Read(p)
+	return f.file.Read(p)
 }
 
 // ReadAt implements billy.File.
 func (f *billyFile) ReadAt(p []byte, off int64) (n int, err error) {
-	return f.ReadAt(p, off)
+	return f.file.ReadAt(p, off)
 }
 
 // Seek implements billy.File.
-func (*billyFile) Seek(offset int64, whence int) (int64, error) {
-	return 0, billyErr(vfs.ErrNotImplemented)
+func (f *billyFile) Seek(offset int64, whence int) (int64, error) {
+	return 0, billyErr(vfs.ErrNotImplemented, f.log)
 }
 
 // Truncate implements billy.File.
-func (*billyFile) Truncate(size int64) error {
-	return billyErr(vfs.ErrNotImplemented)
+func (f *billyFile) Truncate(size int64) error {
+	return billyErr(vfs.ErrNotImplemented, f.log)
 }
 
 // Write implements billy.File.
-func (*billyFile) Write(p []byte) (n int, err error) {
-	return 0, billyErr(vfs.ErrNotImplemented)
+func (f *billyFile) Write(p []byte) (n int, err error) {
+	return 0, billyErr(vfs.ErrNotImplemented, f.log)
 }
 
 // Lock implements billy.File.
@@ -182,9 +188,19 @@ func (*billyFile) Unlock() error {
 	return nil // TODO
 }
 
-func billyErr(err error) error {
-	if err == vfs.ErrNotImplemented {
+func billyErr(err error, log zerolog.Logger) error {
+	if errors.Is(err, vfs.ErrNotImplemented) {
 		return billy.ErrNotSupported
 	}
+	if errors.Is(err, vfs.ErrNotExist) {
+		if err, ok := asErr[*fs.PathError](err); ok {
+			log.Error().Err(err.Err).Str("op", err.Op).Str("path", err.Path).Msg("file not found")
+		}
+		return fs.ErrNotExist
+	}
 	return err
 }
+
+func asErr[E error](err error) (e E, ok bool) {
+	return e, errors.As(err, &e)
+}
diff --git a/src/export/webdav/http.go b/src/export/webdav/http.go
index 80e4fdd..c7d2f0d 100644
--- a/src/export/webdav/http.go
+++ b/src/export/webdav/http.go
@@ -6,6 +6,7 @@ import (
 
 	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
 	"github.com/rs/zerolog/log"
+	"golang.org/x/net/webdav"
 )
 
 func NewWebDAVServer(fs vfs.Filesystem, port int, user, pass string) error {
@@ -36,3 +37,42 @@ func NewWebDAVServer(fs vfs.Filesystem, port int, user, pass string) error {
 
 	return httpServer.ListenAndServe()
 }
+
+func NewDirServer(dir string, port int, user, pass string) error {
+
+	l := log.Logger.With().Str("component", "webDAV").Logger()
+	srv := &webdav.Handler{
+		Prefix:     "/",
+		FileSystem: webdav.Dir(dir),
+		LockSystem: webdav.NewMemLS(),
+		Logger: func(req *http.Request, err error) {
+			if err != nil {
+				l.Error().Err(err).Str("path", req.RequestURI).Msg("webDAV error")
+			}
+		},
+	}
+
+	serveMux := http.NewServeMux()
+
+	serveMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		username, password, _ := r.BasicAuth()
+		if username == user && password == pass {
+			srv.ServeHTTP(w, r)
+			return
+		}
+
+		w.Header().Set("WWW-Authenticate", `Basic realm="BASIC WebDAV REALM"`)
+		w.WriteHeader(401)
+		_, _ = w.Write([]byte("401 Unauthorized\n"))
+	})
+
+	//nolint:exhaustruct
+	httpServer := &http.Server{
+		Addr:    fmt.Sprintf("0.0.0.0:%d", port),
+		Handler: serveMux,
+	}
+
+	log.Info().Str("host", httpServer.Addr).Msg("starting webDAV server")
+
+	return httpServer.ListenAndServe()
+}
diff --git a/src/host/service/service.go b/src/host/service/service.go
index 43e9caa..e48f80a 100644
--- a/src/host/service/service.go
+++ b/src/host/service/service.go
@@ -15,7 +15,7 @@ import (
 
 type Service struct {
 	c   *torrent.Client
-	rep storage.TorrentsRepository
+	rep storage.ExlcudedFiles
 
 	// stats *Stats
 	DefaultPriority types.PiecePriority
@@ -24,7 +24,7 @@ type Service struct {
 	addTimeout, readTimeout int
 }
 
-func NewService(c *torrent.Client, rep storage.TorrentsRepository, addTimeout, readTimeout int) *Service {
+func NewService(c *torrent.Client, rep storage.ExlcudedFiles, addTimeout, readTimeout int) *Service {
 	l := slog.With("component", "torrent-service")
 	return &Service{
 		log:             l,
@@ -39,6 +39,36 @@ func NewService(c *torrent.Client, rep storage.TorrentsRepository, addTimeout, r
 
 var _ vfs.FsFactory = (*Service)(nil).NewTorrentFs
 
+func (s *Service) NewTorrent(f vfs.File) (*torrent.Torrent, error) {
+	ctx, cancel := context.WithTimeout(context.TODO(), time.Second*time.Duration(s.addTimeout))
+	defer cancel()
+	defer f.Close()
+
+	mi, err := metainfo.Load(f)
+	if err != nil {
+		return nil, err
+	}
+
+	t, ok := s.c.Torrent(mi.HashInfoBytes())
+	if !ok {
+		t, err = s.c.AddTorrent(mi)
+		if err != nil {
+			return nil, err
+		}
+		select {
+		case <-ctx.Done():
+			return nil, fmt.Errorf("creating torrent fs timed out")
+		case <-t.GotInfo():
+		}
+		for _, f := range t.Files() {
+			f.SetPriority(s.DefaultPriority)
+		}
+		t.AllowDataDownload()
+	}
+
+	return t, nil
+}
+
 func (s *Service) NewTorrentFs(f vfs.File) (vfs.Filesystem, error) {
 	ctx, cancel := context.WithTimeout(context.TODO(), time.Second*time.Duration(s.addTimeout))
 	defer cancel()
@@ -72,3 +102,7 @@ func (s *Service) NewTorrentFs(f vfs.File) (vfs.Filesystem, error) {
 func (s *Service) Stats() (*Stats, error) {
 	return &Stats{}, nil
 }
+
+func (s *Service) GetStats() torrent.ConnStats {
+	return s.c.ConnStats()
+}
diff --git a/src/host/storage.go b/src/host/storage.go
index bc5bfb0..04af4b4 100644
--- a/src/host/storage.go
+++ b/src/host/storage.go
@@ -5,7 +5,7 @@ import (
 	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
 )
 
-func NewStorage(dataPath string, tsrv *service.Service) vfs.Filesystem {
+func NewTorrentStorage(dataPath string, tsrv *service.Service) vfs.Filesystem {
 	factories := map[string]vfs.FsFactory{
 		".torrent": tsrv.NewTorrentFs,
 	}
diff --git a/src/host/storage/repository.go b/src/host/storage/excluded-files.go
similarity index 93%
rename from src/host/storage/repository.go
rename to src/host/storage/excluded-files.go
index 07985e1..94a75e9 100644
--- a/src/host/storage/repository.go
+++ b/src/host/storage/excluded-files.go
@@ -13,12 +13,12 @@ import (
 	"github.com/philippgille/gokv/encoding"
 )
 
-type TorrentsRepository interface {
+type ExlcudedFiles interface {
 	ExcludeFile(file *torrent.File) error
 	ExcludedFiles(hash metainfo.Hash) ([]string, error)
 }
 
-func NewTorrentMetaRepository(metaDir string, storage atstorage.ClientImplCloser) (TorrentsRepository, error) {
+func NewExcludedFiles(metaDir string, storage atstorage.ClientImplCloser) (ExlcudedFiles, error) {
 	excludedFilesStore, err := badgerdb.NewStore(badgerdb.Options{
 		Dir:   filepath.Join(metaDir, "excluded-files"),
 		Codec: encoding.JSON,
diff --git a/src/host/storage/piece-completion.go b/src/host/storage/piece-completion.go
index b98ebaa..7d727a0 100644
--- a/src/host/storage/piece-completion.go
+++ b/src/host/storage/piece-completion.go
@@ -5,9 +5,11 @@ import (
 	"fmt"
 	"log/slog"
 
+	dlog "git.kmsign.ru/royalcat/tstor/src/log"
 	"github.com/anacrolix/torrent/metainfo"
 	"github.com/anacrolix/torrent/storage"
 	"github.com/dgraph-io/badger/v4"
+	"github.com/rs/zerolog/log"
 )
 
 type PieceCompletionState byte
@@ -32,9 +34,11 @@ type badgerPieceCompletion struct {
 var _ storage.PieceCompletion = (*badgerPieceCompletion)(nil)
 
 func NewBadgerPieceCompletion(dir string) (storage.PieceCompletion, error) {
+	l := log.Logger.With().Str("component", "badger").Str("db", "piece-completion").Logger()
+
 	opts := badger.
 		DefaultOptions(dir).
-		WithLogger(badgerSlog{slog: slog.With("component", "piece-completion")})
+		WithLogger(&dlog.Badger{L: l})
 	db, err := badger.Open(opts)
 	if err != nil {
 		return nil, err
diff --git a/src/host/storage/storage.go b/src/host/storage/storage.go
index 33cfed4..a400426 100644
--- a/src/host/storage/storage.go
+++ b/src/host/storage/storage.go
@@ -10,20 +10,15 @@ import (
 )
 
 func SetupStorage(cfg config.TorrentClient) (storage.ClientImplCloser, storage.PieceCompletion, error) {
-	pcp := filepath.Join(cfg.DataFolder, "piece-completion")
+	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)
 	}
-	pc, err := storage.NewBoltPieceCompletion(pcp)
+	pc, err := NewBadgerPieceCompletion(pcp)
 	if err != nil {
 		return nil, nil, fmt.Errorf("error creating servers piece completion: %w", err)
 	}
 
-	// pc, err := NewBadgerPieceCompletion(pcp)
-	// if err != nil {
-	// 	return nil, nil, fmt.Errorf("error creating servers piece completion: %w", err)
-	// }
-
 	// TODO implement cache/storage switching
 	// cacheDir := filepath.Join(tcfg.DataFolder, "cache")
 	// if err := os.MkdirAll(cacheDir, 0744); err != nil {
@@ -39,19 +34,17 @@ func SetupStorage(cfg config.TorrentClient) (storage.ClientImplCloser, storage.P
 	// rp := storage.NewResourcePieces(fc.AsResourceProvider())
 	// st := &stc{rp}
 
-	// filesDir := filepath.Join(cfg.DataFolder, "files")
-	// if err := os.MkdirAll(filesDir, 0744); err != nil {
-	// 	return nil, nil, fmt.Errorf("error creating piece completion folder: %w", err)
-	// }
-
-	//st := NewFileStorage(filesDir, pc)
-
-	piecesDir := filepath.Join(cfg.DataFolder, "pieces")
-	if err := os.MkdirAll(piecesDir, 0744); err != nil {
+	filesDir := cfg.DataFolder
+	if err := os.MkdirAll(filesDir, 0744); err != nil {
 		return nil, nil, fmt.Errorf("error creating piece completion folder: %w", err)
 	}
+	st := NewFileStorage(filesDir, pc)
 
-	st := storage.NewMMapWithCompletion(piecesDir, pc)
+	// piecesDir := filepath.Join(cfg.DataFolder, ".pieces")
+	// if err := os.MkdirAll(piecesDir, 0744); err != nil {
+	// 	return nil, nil, fmt.Errorf("error creating piece completion folder: %w", err)
+	// }
+	// st := storage.NewMMapWithCompletion(piecesDir, pc)
 
 	return st, pc, nil
 }
diff --git a/src/host/storage/storage_files.go b/src/host/storage/storage_files.go
index 04c837b..52e3515 100644
--- a/src/host/storage/storage_files.go
+++ b/src/host/storage/storage_files.go
@@ -1,55 +1,68 @@
 package storage
 
 import (
-	"fmt"
-	"io"
-	"log"
 	"os"
 	"path"
 	"path/filepath"
-	"strings"
+	"slices"
 
-	"github.com/anacrolix/missinggo"
 	"github.com/anacrolix/torrent"
-	"github.com/anacrolix/torrent/common"
 	"github.com/anacrolix/torrent/metainfo"
-	"github.com/anacrolix/torrent/segments"
 	"github.com/anacrolix/torrent/storage"
+	"github.com/rs/zerolog"
+	"github.com/rs/zerolog/log"
 )
 
 type FileStorageDeleter interface {
 	storage.ClientImplCloser
 	DeleteFile(file *torrent.File) error
+	Cleanup(expected []*torrent.Torrent) error
 }
 
 // NewFileStorage creates a new ClientImplCloser that stores files using the OS native filesystem.
 func NewFileStorage(baseDir string, pc storage.PieceCompletion) FileStorageDeleter {
-	return &FileStorage{baseDir: baseDir, pieceCompletion: pc}
+
+	return &FileStorage{
+		baseDir: baseDir,
+		ClientImplCloser: storage.NewFileOpts(storage.NewFileClientOpts{
+			ClientBaseDir:   baseDir,
+			PieceCompletion: pc,
+			TorrentDirMaker: torrentDir,
+			FilePathMaker: func(opts storage.FilePathMakerOpts) string {
+				return filePath(opts.File)
+			},
+		}),
+		pieceCompletion: pc,
+		log:             log.Logger.With().Str("component", "torrent-client").Logger(),
+	}
 }
 
 // File-based storage for torrents, that isn't yet bound to a particular torrent.
 type FileStorage struct {
-	baseDir         string
+	baseDir string
+	storage.ClientImplCloser
 	pieceCompletion storage.PieceCompletion
+	log             zerolog.Logger
 }
 
 func (me *FileStorage) Close() error {
 	return me.pieceCompletion.Close()
 }
 
-func (me *FileStorage) torrentDir(info *metainfo.Info, infoHash metainfo.Hash) string {
-	return filepath.Join(me.baseDir, info.Name)
+func torrentDir(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string {
+	return filepath.Join(baseDir, info.Name)
 }
 
-func (me *FileStorage) filePath(file metainfo.FileInfo) string {
+func filePath(file *metainfo.FileInfo) string {
 	return filepath.Join(file.Path...)
 }
 
 func (fs *FileStorage) DeleteFile(file *torrent.File) error {
 	info := file.Torrent().Info()
 	infoHash := file.Torrent().InfoHash()
-	torrentDir := fs.torrentDir(info, infoHash)
-	relFilePath := fs.filePath(file.FileInfo())
+	torrentDir := torrentDir(fs.baseDir, info, infoHash)
+	fileInfo := file.FileInfo()
+	relFilePath := filePath(&fileInfo)
 	filePath := path.Join(torrentDir, relFilePath)
 	for i := file.BeginPieceIndex(); i < file.EndPieceIndex(); i++ {
 		pk := metainfo.PieceKey{InfoHash: infoHash, Index: i}
@@ -61,254 +74,32 @@ func (fs *FileStorage) DeleteFile(file *torrent.File) error {
 	return os.Remove(filePath)
 }
 
-func (fs FileStorage) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (storage.TorrentImpl, error) {
-	dir := fs.torrentDir(info, infoHash)
-	upvertedFiles := info.UpvertedFiles()
-	files := make([]file, 0, len(upvertedFiles))
-	for i, fileInfo := range upvertedFiles {
-		filePath := filepath.Join(dir, fs.filePath(fileInfo))
-		if !isSubFilepath(dir, filePath) {
-			return storage.TorrentImpl{}, fmt.Errorf("file %v: path %q is not sub path of %q", i, filePath, fs.baseDir)
-		}
-
-		f := file{
-			path:   filePath,
-			length: fileInfo.Length,
-		}
-		if f.length == 0 {
-			err := CreateNativeZeroLengthFile(f.path)
-			if err != nil {
-				return storage.TorrentImpl{}, fmt.Errorf("creating zero length file: %w", err)
-			}
-		}
-		files = append(files, f)
+func (fs *FileStorage) Cleanup(expected []*torrent.Torrent) error {
+	expectedEntries := []string{}
+	for _, e := range expected {
+		expectedEntries = append(expectedEntries, e.Name())
 	}
-	t := &fileTorrentImpl{
-		files:          files,
-		segmentLocater: segments.NewIndex(common.LengthIterFromUpvertedFiles(upvertedFiles)),
-		infoHash:       infoHash,
-		completion:     fs.pieceCompletion,
+
+	entries, err := os.ReadDir(fs.baseDir)
+	if err != nil {
+		return err
 	}
-	return storage.TorrentImpl{
-		Piece: t.Piece,
-		Close: t.Close,
-	}, nil
-}
 
-type file struct {
-	// The safe, OS-local file path.
-	path   string
-	length int64
-}
-
-type fileTorrentImpl struct {
-	files          []file
-	segmentLocater segments.Index
-	infoHash       metainfo.Hash
-	completion     storage.PieceCompletion
-}
-
-func (fts *fileTorrentImpl) Piece(p metainfo.Piece) storage.PieceImpl {
-	// Create a view onto the file-based torrent storage.
-	_io := fileTorrentImplIO{fts}
-	// Return the appropriate segments of this.
-	return &filePieceImpl{
-		fileTorrentImpl: fts,
-		p:               p,
-		WriterAt:        missinggo.NewSectionWriter(_io, p.Offset(), p.Length()),
-		ReaderAt:        io.NewSectionReader(_io, p.Offset(), p.Length()),
+	toDelete := []string{}
+	for _, v := range entries {
+		if !slices.Contains(expectedEntries, v.Name()) {
+			toDelete = append(toDelete, v.Name())
+		}
 	}
-}
 
-func (fs *fileTorrentImpl) Close() error {
+	fs.log.Info().Int("count", len(toDelete)).Msg("start deleting trash data")
+	for _, name := range toDelete {
+		p := path.Join(fs.baseDir, name)
+		fs.log.Info().Str("path", p).Msg("deleting trash data")
+		err := os.RemoveAll(p)
+		if err != nil {
+			return err
+		}
+	}
 	return nil
 }
-
-// A helper to create zero-length files which won't appear for file-orientated storage since no
-// writes will ever occur to them (no torrent data is associated with a zero-length file). The
-// caller should make sure the file name provided is safe/sanitized.
-func CreateNativeZeroLengthFile(name string) error {
-	err := os.MkdirAll(filepath.Dir(name), 0o777)
-	if err != nil {
-		return err
-	}
-	f, err := os.Create(name)
-	if err != nil {
-		return err
-	}
-	return f.Close()
-}
-
-// Exposes file-based storage of a torrent, as one big ReadWriterAt.
-type fileTorrentImplIO struct {
-	fts *fileTorrentImpl
-}
-
-// Returns EOF on short or missing file.
-func (fst *fileTorrentImplIO) readFileAt(file file, b []byte, off int64) (n int, err error) {
-	f, err := os.Open(file.path)
-	if os.IsNotExist(err) {
-		// File missing is treated the same as a short file.
-		err = io.EOF
-		return
-	}
-	if err != nil {
-		return
-	}
-	defer f.Close()
-	// Limit the read to within the expected bounds of this file.
-	if int64(len(b)) > file.length-off {
-		b = b[:file.length-off]
-	}
-	for off < file.length && len(b) != 0 {
-		n1, err1 := f.ReadAt(b, off)
-		b = b[n1:]
-		n += n1
-		off += int64(n1)
-		if n1 == 0 {
-			err = err1
-			break
-		}
-	}
-	return
-}
-
-// Only returns EOF at the end of the torrent. Premature EOF is ErrUnexpectedEOF.
-func (fst fileTorrentImplIO) ReadAt(b []byte, off int64) (n int, err error) {
-	fst.fts.segmentLocater.Locate(
-		segments.Extent{Start: off, Length: int64(len(b))},
-		func(i int, e segments.Extent) bool {
-			n1, err1 := fst.readFileAt(fst.fts.files[i], b[:e.Length], e.Start)
-			n += n1
-			b = b[n1:]
-			err = err1
-			return err == nil // && int64(n1) == e.Length
-		},
-	)
-	if len(b) != 0 && err == nil {
-		err = io.EOF
-	}
-	return
-}
-
-func (fst fileTorrentImplIO) WriteAt(p []byte, off int64) (n int, err error) {
-	// log.Printf("write at %v: %v bytes", off, len(p))
-	fst.fts.segmentLocater.Locate(
-		segments.Extent{Start: off, Length: int64(len(p))},
-		func(i int, e segments.Extent) bool {
-			name := fst.fts.files[i].path
-			err = os.MkdirAll(filepath.Dir(name), 0o777)
-			if err != nil {
-				return false
-			}
-			var f *os.File
-			f, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0o666)
-			if err != nil {
-				return false
-			}
-			var n1 int
-			n1, err = f.WriteAt(p[:e.Length], e.Start)
-			// log.Printf("%v %v wrote %v: %v", i, e, n1, err)
-			closeErr := f.Close()
-			n += n1
-			p = p[n1:]
-			if err == nil {
-				err = closeErr
-			}
-			if err == nil && int64(n1) != e.Length {
-				err = io.ErrShortWrite
-			}
-			return err == nil
-		},
-	)
-	return n, err
-}
-
-type filePieceImpl struct {
-	*fileTorrentImpl
-	p metainfo.Piece
-	io.WriterAt
-	io.ReaderAt
-}
-
-var _ storage.PieceImpl = (*filePieceImpl)(nil)
-
-func (me *filePieceImpl) pieceKey() metainfo.PieceKey {
-	return metainfo.PieceKey{InfoHash: me.infoHash, Index: me.p.Index()}
-}
-
-func (fs *filePieceImpl) Completion() storage.Completion {
-	c, err := fs.completion.Get(fs.pieceKey())
-	if err != nil {
-		log.Printf("error getting piece completion: %s", err)
-		c.Ok = false
-		return c
-	}
-
-	verified := true
-	if c.Complete {
-		// If it's allegedly complete, check that its constituent files have the necessary length.
-		for _, fi := range extentCompleteRequiredLengths(fs.p.Info, fs.p.Offset(), fs.p.Length()) {
-			s, err := os.Stat(fs.files[fi.fileIndex].path)
-			if err != nil || s.Size() < fi.length {
-				verified = false
-				break
-			}
-		}
-	}
-
-	if !verified {
-		// The completion was wrong, fix it.
-		c.Complete = false
-		fs.completion.Set(fs.pieceKey(), false)
-	}
-
-	return c
-}
-
-func (fs *filePieceImpl) MarkComplete() error {
-	return fs.completion.Set(fs.pieceKey(), true)
-}
-
-func (fs *filePieceImpl) MarkNotComplete() error {
-	return fs.completion.Set(fs.pieceKey(), false)
-}
-
-type requiredLength struct {
-	fileIndex int
-	length    int64
-}
-
-func isSubFilepath(base, sub string) bool {
-	rel, err := filepath.Rel(base, sub)
-	if err != nil {
-		return false
-	}
-	return rel != ".." && !strings.HasPrefix(rel, ".."+string(os.PathSeparator))
-}
-
-func extentCompleteRequiredLengths(info *metainfo.Info, off, n int64) (ret []requiredLength) {
-	if n == 0 {
-		return
-	}
-	for i, fi := range info.UpvertedFiles() {
-		if off >= fi.Length {
-			off -= fi.Length
-			continue
-		}
-		n1 := n
-		if off+n1 > fi.Length {
-			n1 = fi.Length - off
-		}
-		ret = append(ret, requiredLength{
-			fileIndex: i,
-			length:    off + n1,
-		})
-		n -= n1
-		if n == 0 {
-			return
-		}
-		off = 0
-	}
-	panic("extent exceeds torrent bounds")
-}
diff --git a/src/host/vfs/os.go b/src/host/vfs/os.go
index 3ec6ad5..658e64d 100644
--- a/src/host/vfs/os.go
+++ b/src/host/vfs/os.go
@@ -11,11 +11,6 @@ type OsFS struct {
 	hostDir string
 }
 
-// Unlink implements Filesystem.
-func (fs *OsFS) Unlink(filename string) error {
-	return fs.Unlink(filename)
-}
-
 // Stat implements Filesystem.
 func (fs *OsFS) Stat(filename string) (fs.FileInfo, error) {
 	if path.Clean(filename) == Separator {
@@ -25,6 +20,11 @@ func (fs *OsFS) Stat(filename string) (fs.FileInfo, error) {
 	return os.Stat(path.Join(fs.hostDir, filename))
 }
 
+// Unlink implements Filesystem.
+func (fs *OsFS) Unlink(filename string) error {
+	return os.RemoveAll(path.Join(fs.hostDir, filename))
+}
+
 // Open implements Filesystem.
 func (fs *OsFS) Open(filename string) (File, error) {
 	if path.Clean(filename) == Separator {
diff --git a/src/host/vfs/torrent.go b/src/host/vfs/torrent.go
index 43a6f21..690768b 100644
--- a/src/host/vfs/torrent.go
+++ b/src/host/vfs/torrent.go
@@ -22,7 +22,7 @@ var _ Filesystem = &TorrentFs{}
 type TorrentFs struct {
 	mu  sync.Mutex
 	t   *torrent.Torrent
-	rep storage.TorrentsRepository
+	rep storage.ExlcudedFiles
 
 	readTimeout int
 
@@ -32,7 +32,7 @@ type TorrentFs struct {
 	resolver *resolver
 }
 
-func NewTorrentFs(t *torrent.Torrent, rep storage.TorrentsRepository, readTimeout int) *TorrentFs {
+func NewTorrentFs(t *torrent.Torrent, rep storage.ExlcudedFiles, readTimeout int) *TorrentFs {
 	return &TorrentFs{
 		t:           t,
 		rep:         rep,
@@ -54,29 +54,41 @@ func (fs *TorrentFs) files() (map[string]*torrentFile, error) {
 
 		fs.filesCache = make(map[string]*torrentFile)
 		for _, file := range files {
-
 			p := file.Path()
 
 			if slices.Contains(excludedFiles, p) {
 				continue
 			}
+
 			if strings.Contains(p, "/.pad/") {
 				continue
 			}
 
 			p = AbsPath(file.Path())
 
-			// TODO make optional
-			// removing the torrent root directory of same name  as torrent
-			p, _ = strings.CutPrefix(p, "/"+fs.t.Name()+"/")
-			p = AbsPath(p)
-
 			fs.filesCache[p] = &torrentFile{
 				name:    path.Base(p),
 				timeout: fs.readTimeout,
 				file:    file,
 			}
 		}
+
+		rootDir := "/" + fs.t.Name() + "/"
+		singleDir := true
+		for k, _ := range fs.filesCache {
+			if !strings.HasPrefix(k, rootDir) {
+				singleDir = false
+			}
+		}
+		if singleDir {
+			for k, f := range fs.filesCache {
+				delete(fs.filesCache, k)
+				k, _ = strings.CutPrefix(k, rootDir)
+				k = AbsPath(k)
+				fs.filesCache[k] = f
+			}
+		}
+
 		fs.mu.Unlock()
 	}
 
diff --git a/src/log/badger.go b/src/log/badger.go
index b6bc40f..76d80ea 100644
--- a/src/log/badger.go
+++ b/src/log/badger.go
@@ -3,9 +3,12 @@ package log
 import (
 	"strings"
 
+	"github.com/dgraph-io/badger/v4"
 	"github.com/rs/zerolog"
 )
 
+var _ badger.Logger = (*Badger)(nil)
+
 type Badger struct {
 	L zerolog.Logger
 }
diff --git a/src/log/nfs.go b/src/log/nfs.go
new file mode 100644
index 0000000..422b6b9
--- /dev/null
+++ b/src/log/nfs.go
@@ -0,0 +1,173 @@
+package log
+
+import (
+	"fmt"
+
+	"github.com/rs/zerolog"
+	nfs "github.com/willscott/go-nfs"
+)
+
+var _ nfs.Logger = (*NFSLog)(nil)
+
+type NFSLog struct {
+	r zerolog.Logger
+	l zerolog.Logger
+}
+
+func NewNFSLog(r zerolog.Logger) nfs.Logger {
+	return &NFSLog{
+		r: r,
+		l: r.Level(zerolog.DebugLevel),
+	}
+}
+
+// Debug implements nfs.Logger.
+func (l *NFSLog) Debug(args ...interface{}) {
+	l.l.Debug().Msg(fmt.Sprint(args...))
+}
+
+// Debugf implements nfs.Logger.
+func (l *NFSLog) Debugf(format string, args ...interface{}) {
+	l.l.Debug().Msgf(format, args...)
+}
+
+// Error implements nfs.Logger.
+func (l *NFSLog) Error(args ...interface{}) {
+	l.l.Error().Msg(fmt.Sprint(args...))
+}
+
+// Errorf implements nfs.Logger.
+func (l *NFSLog) Errorf(format string, args ...interface{}) {
+	l.l.Error().Msgf(format, args...)
+}
+
+// Fatal implements nfs.Logger.
+func (l *NFSLog) Fatal(args ...interface{}) {
+	l.l.Fatal().Msg(fmt.Sprint(args...))
+}
+
+// Fatalf implements nfs.Logger.
+func (l *NFSLog) Fatalf(format string, args ...interface{}) {
+	l.l.Fatal().Msgf(format, args...)
+}
+
+// Info implements nfs.Logger.
+func (l *NFSLog) Info(args ...interface{}) {
+	l.l.Info().Msg(fmt.Sprint(args...))
+}
+
+// Infof implements nfs.Logger.
+func (l *NFSLog) Infof(format string, args ...interface{}) {
+	l.l.Info().Msgf(format, args...)
+}
+
+// Panic implements nfs.Logger.
+func (l *NFSLog) Panic(args ...interface{}) {
+	l.l.Panic().Msg(fmt.Sprint(args...))
+}
+
+// Panicf implements nfs.Logger.
+func (l *NFSLog) Panicf(format string, args ...interface{}) {
+	l.l.Panic().Msgf(format, args...)
+}
+
+// Print implements nfs.Logger.
+func (l *NFSLog) Print(args ...interface{}) {
+	l.l.Print(args...)
+}
+
+// Printf implements nfs.Logger.
+func (l *NFSLog) Printf(format string, args ...interface{}) {
+	l.l.Printf(format, args...)
+}
+
+// Trace implements nfs.Logger.
+func (l *NFSLog) Trace(args ...interface{}) {
+	l.l.Trace().Msg(fmt.Sprint(args...))
+}
+
+// Tracef implements nfs.Logger.
+func (l *NFSLog) Tracef(format string, args ...interface{}) {
+	l.l.Trace().Msgf(format, args...)
+}
+
+// Warn implements nfs.Logger.
+func (l *NFSLog) Warn(args ...interface{}) {
+	l.l.Warn().Msg(fmt.Sprint(args...))
+}
+
+// Warnf implements nfs.Logger.
+func (l *NFSLog) Warnf(format string, args ...interface{}) {
+	l.l.Warn().Msgf(format, args...)
+}
+
+// GetLevel implements nfs.Logger.
+func (l *NFSLog) GetLevel() nfs.LogLevel {
+	zl := l.l.GetLevel()
+	switch zl {
+	case zerolog.PanicLevel, zerolog.Disabled:
+		return nfs.PanicLevel
+	case zerolog.FatalLevel:
+		return nfs.FatalLevel
+	case zerolog.ErrorLevel:
+		return nfs.ErrorLevel
+	case zerolog.WarnLevel:
+		return nfs.WarnLevel
+	case zerolog.InfoLevel:
+		return nfs.InfoLevel
+	case zerolog.DebugLevel:
+		return nfs.DebugLevel
+	case zerolog.TraceLevel:
+		return nfs.TraceLevel
+	}
+	return nfs.DebugLevel
+}
+
+// ParseLevel implements nfs.Logger.
+func (l *NFSLog) ParseLevel(level string) (nfs.LogLevel, error) {
+	switch level {
+	case "panic":
+		return nfs.PanicLevel, nil
+	case "fatal":
+		return nfs.FatalLevel, nil
+	case "error":
+		return nfs.ErrorLevel, nil
+	case "warn":
+		return nfs.WarnLevel, nil
+	case "info":
+		return nfs.InfoLevel, nil
+	case "debug":
+		return nfs.DebugLevel, nil
+	case "trace":
+		return nfs.TraceLevel, nil
+	}
+	var ll nfs.LogLevel
+	return ll, fmt.Errorf("invalid log level %q", level)
+}
+
+// SetLevel implements nfs.Logger.
+func (l *NFSLog) SetLevel(level nfs.LogLevel) {
+	switch level {
+	case nfs.PanicLevel:
+		l.l = l.r.Level(zerolog.PanicLevel)
+		return
+	case nfs.FatalLevel:
+		l.l = l.r.Level(zerolog.FatalLevel)
+		return
+	case nfs.ErrorLevel:
+		l.l = l.r.Level(zerolog.ErrorLevel)
+		return
+	case nfs.WarnLevel:
+		l.l = l.r.Level(zerolog.WarnLevel)
+		return
+	case nfs.InfoLevel:
+		l.l = l.r.Level(zerolog.InfoLevel)
+		return
+	case nfs.DebugLevel:
+		l.l = l.r.Level(zerolog.DebugLevel)
+		return
+	case nfs.TraceLevel:
+		l.l = l.r.Level(zerolog.TraceLevel)
+		return
+	}
+}
diff --git a/src/proto/gen.go b/src/proto/gen.go
new file mode 100644
index 0000000..55007dd
--- /dev/null
+++ b/src/proto/gen.go
@@ -0,0 +1,3 @@
+package proto
+
+//go:generate protoc --go_out=. --go_opt=paths=source_relative --go_opt=Mtstor.proto=git.kmsign.ru/royalcat/tstor/src/proto --go-grpc_out=. --go-grpc_opt=paths=source_relative --go-grpc_opt=Mtstor.proto=git.kmsign.ru/royalcat/tstor/src/proto  --proto_path=../../proto tstor.proto

From b97dcc8d8f234864eee75b2f9c6cecc5da74a560 Mon Sep 17 00:00:00 2001
From: royalcat <k.adamovich20@gmail.com>
Date: Sun, 28 Jan 2024 23:22:49 +0300
Subject: [PATCH 11/18] wip

---
 .gqlgen.yml                                   |   44 +
 .graphqlrc.yaml                               |    4 +
 cmd/generate-graphlq/main.go                  |   65 +
 cmd/tstor/main.go                             |   82 +-
 go.mod                                        |   22 +-
 go.sum                                        |   60 +-
 graphql/mutation.graphql                      |   10 +
 graphql/query.graphql                         |   43 +
 graphql/schema.graphql                        |    9 +
 graphql/types/torrent.graphql                 |   24 +
 src/delivery/graphql/generated.go             | 5741 +++++++++++++++++
 src/delivery/graphql/model/filter.go          |   21 +
 src/delivery/graphql/model/mappers.go         |   24 +
 src/delivery/graphql/model/models_gen.go      |   93 +
 src/delivery/graphql/oneof.go                 |   28 +
 .../graphql/resolver/mutation.resolvers.go    |   64 +
 .../graphql/resolver/query.resolvers.go       |   75 +
 src/delivery/graphql/resolver/resolver.go     |   11 +
 .../graphql/resolver/torrent.resolvers.go     |   73 +
 src/delivery/router.go                        |   35 +
 src/export/fuse/handler.go                    |   15 +-
 src/export/fuse/mount.go                      |   34 +-
 src/export/nfs/handler.go                     |    5 +-
 src/export/nfs/wrapper-v3.go                  |   18 +-
 src/export/webdav/handler.go                  |    6 +-
 src/export/webdav/http.go                     |   10 +-
 src/host/controller/torrent.go                |  107 +
 .../storage.go => filestorage/setup.go}       |    7 +-
 src/host/filestorage/storage_files.go         |  186 +
 src/host/service/service.go                   |  181 +-
 src/host/storage/client.go                    |   36 -
 src/host/storage/storage_files.go             |  105 -
 src/host/store/client.go                      |   47 +
 src/host/{storage => store}/excluded-files.go |   32 +-
 src/host/{storage => store}/id.go             |    2 +-
 src/host/store/info.go                        |   79 +
 .../{storage => store}/piece-completion.go    |    5 +-
 src/host/{storage => store}/store.go          |    6 +-
 src/host/vfs/archive.go                       |   84 +-
 src/host/vfs/archive_test.go                  |    3 +-
 src/host/vfs/log.go                           |  117 +
 src/host/vfs/os.go                            |   11 +-
 src/host/vfs/resolver.go                      |   28 +-
 src/host/vfs/torrent.go                       |  152 +-
 src/host/vfs/utils.go                         |   30 +-
 src/http/http.go                              |   16 +-
 src/log/badger.go                             |   17 +-
 src/log/log.go                                |   57 +-
 src/log/nfs.go                                |  169 +-
 src/log/torrent.go                            |   20 +-
 src/proto/gen.go                              |    3 -
 tools.go                                      |    9 +
 52 files changed, 7570 insertions(+), 555 deletions(-)
 create mode 100644 .gqlgen.yml
 create mode 100644 .graphqlrc.yaml
 create mode 100644 cmd/generate-graphlq/main.go
 create mode 100644 graphql/mutation.graphql
 create mode 100644 graphql/query.graphql
 create mode 100644 graphql/schema.graphql
 create mode 100644 graphql/types/torrent.graphql
 create mode 100644 src/delivery/graphql/generated.go
 create mode 100644 src/delivery/graphql/model/filter.go
 create mode 100644 src/delivery/graphql/model/mappers.go
 create mode 100644 src/delivery/graphql/model/models_gen.go
 create mode 100644 src/delivery/graphql/oneof.go
 create mode 100644 src/delivery/graphql/resolver/mutation.resolvers.go
 create mode 100644 src/delivery/graphql/resolver/query.resolvers.go
 create mode 100644 src/delivery/graphql/resolver/resolver.go
 create mode 100644 src/delivery/graphql/resolver/torrent.resolvers.go
 create mode 100644 src/delivery/router.go
 create mode 100644 src/host/controller/torrent.go
 rename src/host/{storage/storage.go => filestorage/setup.go} (88%)
 create mode 100644 src/host/filestorage/storage_files.go
 delete mode 100644 src/host/storage/client.go
 delete mode 100644 src/host/storage/storage_files.go
 create mode 100644 src/host/store/client.go
 rename src/host/{storage => store}/excluded-files.go (68%)
 rename src/host/{storage => store}/id.go (96%)
 create mode 100644 src/host/store/info.go
 rename src/host/{storage => store}/piece-completion.go (95%)
 rename src/host/{storage => store}/store.go (94%)
 create mode 100644 src/host/vfs/log.go
 delete mode 100644 src/proto/gen.go
 create mode 100644 tools.go

diff --git a/.gqlgen.yml b/.gqlgen.yml
new file mode 100644
index 0000000..9f3f595
--- /dev/null
+++ b/.gqlgen.yml
@@ -0,0 +1,44 @@
+schema:
+  - graphql/*.graphql
+  - graphql/**/*.graphql
+
+exec:
+  filename: src/delivery/graphql/generated.go
+  package: graph
+
+model:
+  filename: src/delivery/graphql/model/models_gen.go
+  package: model
+
+resolver:
+  layout: follow-schema
+  dir: src/delivery/graphql/resolver
+  package: resolver
+  filename_template: "{name}.resolvers.go"
+
+models:
+  DateTime:
+    model: github.com/99designs/gqlgen/graphql.Time
+  Int:
+    model: github.com/99designs/gqlgen/graphql.Int64
+  Torrent:
+    fields:
+      name:
+        resolver: true
+      files:
+        resolver: true
+      excludedFiles:
+        resolver: true
+      peers:
+        resolver: true
+    extraFields:
+      T:
+        type: "*git.kmsign.ru/royalcat/tstor/src/host/controller.Torrent"
+  TorrentFile:
+    extraFields:
+      F:
+        type: "*github.com/anacrolix/torrent.File"
+  TorrentPeer:
+    extraFields:
+      F:
+        type: "*github.com/anacrolix/torrent.PeerConn"
diff --git a/.graphqlrc.yaml b/.graphqlrc.yaml
new file mode 100644
index 0000000..37aea9c
--- /dev/null
+++ b/.graphqlrc.yaml
@@ -0,0 +1,4 @@
+schema:
+    - graphql/schema.graphql
+    - graphql/*.graphql
+    - graphql/**/*.graphql
diff --git a/cmd/generate-graphlq/main.go b/cmd/generate-graphlq/main.go
new file mode 100644
index 0000000..97ef427
--- /dev/null
+++ b/cmd/generate-graphlq/main.go
@@ -0,0 +1,65 @@
+// https://github.com/99designs/gqlgen/issues/2281#issuecomment-1506561381
+package main
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/99designs/gqlgen/api"
+	"github.com/99designs/gqlgen/codegen"
+	"github.com/99designs/gqlgen/codegen/config"
+)
+
+type plugin_ struct {
+}
+
+func (plugin_) Name() string {
+	return "Fix Directive hook called with wrong object"
+}
+
+func (plugin_) GenerateCode(cfg *codegen.Data) error {
+	for _, input := range cfg.Inputs {
+		for _, field := range input.Fields {
+			if field.GoFieldType == codegen.GoFieldVariable {
+				directiveMap := make(map[string]int, len(field.TypeReference.Definition.Directives)+len(field.Object.Directives))
+				for _, v := range field.TypeReference.Definition.Directives {
+					directiveMap[v.Name]++
+				}
+				// for _, v := range field.Object.Directives {
+				// 	directiveMap[v.Name]++
+				// }
+
+				directive := make([]*codegen.Directive, 0, len(field.Directives))
+				for _, v := range field.Directives {
+					if count := directiveMap[v.Name]; count > 0 {
+						directiveMap[v.Name] = count - 1
+						fmt.Printf("Ignore field %s{%s} directive: @%s\n", input.Name, field.Name, v.Name)
+						continue
+					}
+
+					directive = append(directive, v)
+				}
+
+				field.Directives = directive
+			}
+		}
+	}
+
+	return nil
+}
+
+func main() {
+	cfg, err := config.LoadConfigFromDefaultLocations()
+	if err != nil {
+		fmt.Fprintln(os.Stderr, "failed to load config", err.Error())
+		os.Exit(2)
+	}
+
+	err = api.Generate(cfg,
+		api.AddPlugin(&plugin_{}),
+	)
+	if err != nil {
+		fmt.Fprintln(os.Stderr, err.Error())
+		os.Exit(3)
+	}
+}
diff --git a/cmd/tstor/main.go b/cmd/tstor/main.go
index 0050c20..fe63a17 100644
--- a/cmd/tstor/main.go
+++ b/cmd/tstor/main.go
@@ -2,6 +2,7 @@ package main
 
 import (
 	"fmt"
+	"log/slog"
 
 	"net"
 	nethttp "net/http"
@@ -14,9 +15,10 @@ import (
 
 	"git.kmsign.ru/royalcat/tstor/src/config"
 	"git.kmsign.ru/royalcat/tstor/src/host"
+	"git.kmsign.ru/royalcat/tstor/src/host/filestorage"
 	"git.kmsign.ru/royalcat/tstor/src/host/service"
-	"git.kmsign.ru/royalcat/tstor/src/host/storage"
-	"github.com/rs/zerolog/log"
+	"git.kmsign.ru/royalcat/tstor/src/host/store"
+	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
 	"github.com/urfave/cli/v2"
 	wnfs "github.com/willscott/go-nfs"
 
@@ -54,65 +56,70 @@ func main() {
 	}
 
 	if err := app.Run(os.Args); err != nil {
-		log.Fatal().Err(err).Msg("problem starting application")
+		print("problem starting application: ", err.Error())
 	}
 }
 
 func run(configPath string) error {
-
 	conf, err := config.Load(configPath)
 	if err != nil {
 		return fmt.Errorf("error loading configuration: %w", err)
 	}
-
 	dlog.Load(&conf.Log)
-	log := log.Logger.With().Str("conponent", "run").Logger()
+
+	log := slog.Default().With("component", "run")
 
 	// TODO make optional
 	err = syscall.Setpriority(syscall.PRIO_PGRP, 0, 19)
 	if err != nil {
-		log.Err(err).Msg("set priority failed")
+		log.Error("set priority failed", "error", err)
 	}
 
 	if err := os.MkdirAll(conf.TorrentClient.MetadataFolder, 0744); err != nil {
 		return fmt.Errorf("error creating metadata folder: %w", err)
 	}
 
-	fis, err := storage.NewFileItemStore(filepath.Join(conf.TorrentClient.MetadataFolder, "items"), 2*time.Hour)
+	fis, err := store.NewFileItemStore(filepath.Join(conf.TorrentClient.MetadataFolder, "items"), 2*time.Hour)
 	if err != nil {
 		return fmt.Errorf("error starting item store: %w", err)
 	}
 	defer fis.Close()
 
-	id, err := storage.GetOrCreatePeerID(filepath.Join(conf.TorrentClient.MetadataFolder, "ID"))
+	id, err := store.GetOrCreatePeerID(filepath.Join(conf.TorrentClient.MetadataFolder, "ID"))
 	if err != nil {
 		return fmt.Errorf("error creating node ID: %w", err)
 	}
 
-	st, _, err := storage.SetupStorage(conf.TorrentClient)
+	st, _, err := filestorage.Setup(conf.TorrentClient)
 	if err != nil {
 		return err
 	}
 	defer st.Close()
 
-	rep, err := storage.NewExcludedFiles(conf.TorrentClient.MetadataFolder, st)
+	excludedFilesStore, err := store.NewExcludedFiles(conf.TorrentClient.MetadataFolder, st)
 	if err != nil {
 		return err
 	}
 
-	c, err := storage.NewClient(st, fis, &conf.TorrentClient, id)
+	infoBytesStore, err := store.NewInfoBytes(conf.TorrentClient.MetadataFolder)
+	if err != nil {
+		return err
+	}
+
+	c, err := store.NewClient(st, fis, &conf.TorrentClient, id)
 	if err != nil {
 		return fmt.Errorf("error starting torrent client: %w", err)
 	}
 	c.AddDhtNodes(conf.TorrentClient.DHTNodes)
 	defer c.Close()
 
-	ts := service.NewService(c, rep, conf.TorrentClient.AddTimeout, conf.TorrentClient.ReadTimeout)
+	ts := service.NewService(conf.SourceDir, c, st, excludedFilesStore, infoBytesStore, conf.TorrentClient.AddTimeout, conf.TorrentClient.ReadTimeout)
 
 	if err := os.MkdirAll(conf.SourceDir, 0744); err != nil {
 		return fmt.Errorf("error creating data folder: %w", err)
 	}
 	sfs := host.NewTorrentStorage(conf.SourceDir, ts)
+	sfs = vfs.WrapLogFS(sfs, slog.Default())
 
 	// TODO make separate function
 	// {
@@ -161,10 +168,10 @@ func run(configPath string) error {
 	if conf.Mounts.WebDAV.Enabled {
 		go func() {
 			if err := webdav.NewWebDAVServer(sfs, conf.Mounts.WebDAV.Port, conf.Mounts.WebDAV.User, conf.Mounts.WebDAV.Pass); err != nil {
-				log.Error().Err(err).Msg("error starting webDAV")
+				log.Error("error starting webDAV", "error", err)
 			}
 
-			log.Warn().Msg("webDAV configuration not found!")
+			log.Warn("webDAV configuration not found!")
 		}()
 	}
 	if conf.Mounts.HttpFs.Enabled {
@@ -172,7 +179,7 @@ func run(configPath string) error {
 			httpfs := httpfs.NewHTTPFS(sfs)
 			err = nethttp.ListenAndServe(fmt.Sprintf("0.0.0.0:%d", conf.Mounts.HttpFs.Port), nethttp.FileServer(httpfs))
 			if err != nil {
-				log.Error().Err(err).Msg("error starting HTTPFS")
+				log.Error("error starting HTTPFS", "error", err)
 			}
 			// r := gin.New()
 
@@ -181,7 +188,7 @@ func run(configPath string) error {
 			// 	c.FileFromFS(path, httpfs)
 			// })
 
-			log.Info().Str("host", fmt.Sprintf("0.0.0.0:%d", conf.Mounts.HttpFs.Port)).Msg("starting HTTPFS")
+			log.Info("starting HTTPFS", "host", fmt.Sprintf("0.0.0.0:%d", conf.Mounts.HttpFs.Port))
 			// if err := r.Run(fmt.Sprintf("0.0.0.0:%d", conf.Mounts.HttpFs.Port)); err != nil {
 			// 	log.Error().Err(err).Msg("error starting HTTPFS")
 			// }
@@ -190,28 +197,41 @@ func run(configPath string) error {
 
 	if conf.Mounts.NFS.Enabled {
 		go func() {
+			log := log.With("component", "NFS")
 			listener, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", conf.Mounts.NFS.Port))
-			panicOnErr(err, "starting TCP listener")
-			log.Info().Str("host", listener.Addr().String()).Msg("starting NFS server")
+			if err != nil {
+				log.Error("failed to start TCP  listener", err)
+				return
+			}
+			log.Info("starting NFS server", "host", listener.Addr().String())
 			handler, err := nfs.NewNFSv3Handler(sfs)
-			panicOnErr(err, "creating NFS handler")
-			panicOnErr(wnfs.Serve(listener, handler), "serving nfs")
+			if err != nil {
+				log.Error("failed to create NFS handler", "error", err)
+				return
+			}
+
+			err = wnfs.Serve(listener, handler)
+			if err != nil {
+				log.Error("error serving nfs", "error", err)
+				return
+			}
 		}()
 	}
 
 	go func() {
-		if err := webdav.NewDirServer(conf.SourceDir, 36912, conf.Mounts.WebDAV.User, conf.Mounts.WebDAV.Pass); err != nil {
-			log.Error().Err(err).Msg("error starting webDAV")
+		err := webdav.NewDirServer(conf.SourceDir, 36912, conf.Mounts.WebDAV.User, conf.Mounts.WebDAV.Pass)
+		if err != nil {
+			log.Error("error starting webDAV", "error", err)
 		}
-
-		log.Warn().Msg("webDAV configuration not found!")
 	}()
 
 	go func() {
 		logFilename := filepath.Join(conf.Log.Path, dlog.FileName)
 
-		err = http.New(nil, service.NewStats(), ts, logFilename, conf)
-		log.Error().Err(err).Msg("error initializing HTTP server")
+		err := http.New(nil, service.NewStats(), ts, logFilename, conf)
+		if err != nil {
+			log.Error("error initializing HTTP server", "error", err)
+		}
 	}()
 
 	sigChan := make(chan os.Signal, 1)
@@ -220,11 +240,3 @@ func run(configPath string) error {
 
 	return nil
 }
-
-func panicOnErr(err error, desc string) {
-	if err == nil {
-		return
-	}
-	log.Err(err).Msg(desc)
-	log.Panic()
-}
diff --git a/go.mod b/go.mod
index fb88c2c..8477be3 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module git.kmsign.ru/royalcat/tstor
 go 1.21
 
 require (
+	github.com/99designs/gqlgen v0.17.43
 	github.com/anacrolix/dht/v2 v2.21.0
 	github.com/anacrolix/log v0.14.5
 	github.com/anacrolix/missinggo/v2 v2.7.3
@@ -18,26 +19,24 @@ require (
 	github.com/knadh/koanf/providers/file v0.1.0
 	github.com/knadh/koanf/providers/structs v0.1.0
 	github.com/knadh/koanf/v2 v2.0.1
-	github.com/mattn/go-colorable v0.1.13
+	github.com/lmittmann/tint v1.0.4
 	github.com/nwaples/rardecode/v2 v2.0.0-beta.2
 	github.com/philippgille/gokv v0.6.0
 	github.com/philippgille/gokv/badgerdb v0.6.0
 	github.com/philippgille/gokv/encoding v0.6.0
-	github.com/rs/zerolog v1.31.0
 	github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c
 	github.com/stretchr/testify v1.8.4
 	github.com/urfave/cli/v2 v2.27.0
+	github.com/vektah/gqlparser/v2 v2.5.11
 	github.com/willscott/go-nfs v0.0.2
 	golang.org/x/exp v0.0.0-20231226003508-02704c960a9b
 	golang.org/x/net v0.19.0
-	google.golang.org/grpc v1.53.0
-	google.golang.org/protobuf v1.30.0
-	gopkg.in/natefinch/lumberjack.v2 v2.2.1
 )
 
 require (
 	github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
 	github.com/RoaringBitmap/roaring v1.2.3 // indirect
+	github.com/agnivade/levenshtein v1.1.1 // indirect
 	github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect
 	github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
 	github.com/anacrolix/chansync v0.3.0 // indirect
@@ -99,7 +98,7 @@ require (
 	github.com/klauspost/cpuid/v2 v2.2.4 // indirect
 	github.com/knadh/koanf/maps v0.1.1 // indirect
 	github.com/leodido/go-urn v1.2.4 // indirect
-	github.com/mattn/go-isatty v0.0.19 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/mitchellh/copystructure v1.2.0 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/mitchellh/reflectwalk v1.0.2 // indirect
@@ -134,6 +133,7 @@ require (
 	github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect
+	github.com/sosodev/duration v1.1.0 // indirect
 	github.com/tidwall/btree v1.6.0 // indirect
 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
 	github.com/ugorji/go/codec v1.2.11 // indirect
@@ -146,12 +146,14 @@ require (
 	go.opentelemetry.io/otel/trace v1.8.0 // indirect
 	go4.org v0.0.0-20200411211856-f5505b9728dd // indirect
 	golang.org/x/arch v0.3.0 // indirect
-	golang.org/x/crypto v0.16.0 // indirect
-	golang.org/x/sync v0.3.0 // indirect
+	golang.org/x/crypto v0.17.0 // indirect
+	golang.org/x/mod v0.14.0 // indirect
+	golang.org/x/sync v0.5.0 // indirect
 	golang.org/x/sys v0.16.0 // indirect
 	golang.org/x/text v0.14.0 // indirect
-	golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
-	google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
+	golang.org/x/time v0.5.0 // indirect
+	golang.org/x/tools v0.16.0 // indirect
+	google.golang.org/protobuf v1.30.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	modernc.org/libc v1.22.3 // indirect
 	modernc.org/mathutil v1.5.0 // indirect
diff --git a/go.sum b/go.sum
index 5bff810..acc9f71 100644
--- a/go.sum
+++ b/go.sum
@@ -19,6 +19,8 @@ crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU=
 filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
+github.com/99designs/gqlgen v0.17.43 h1:I4SYg6ahjowErAQcHFVKy5EcWuwJ3+Xw9z2fLpuFCPo=
+github.com/99designs/gqlgen v0.17.43/go.mod h1:lO0Zjy8MkZgBdv4T1U91x09r0e0WFOdhVUutlQs1Rsc=
 github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
 github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
 github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
@@ -31,6 +33,8 @@ github.com/RoaringBitmap/roaring v1.2.3 h1:yqreLINqIrX22ErkKI0vY47/ivtJr6n+kMhVO
 github.com/RoaringBitmap/roaring v1.2.3/go.mod h1:plvDsJQpxOC5bw8LRteu/MLWHsHez/3y6cubLI4/1yE=
 github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
 github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
+github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=
+github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
 github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 h1:byYvvbfSo3+9efR4IeReh77gVs4PnNDR3AMOE9NJ7a0=
 github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0/go.mod h1:q37NoqncT41qKc048STsifIt69LfUJ8SrWWcz/yam5k=
 github.com/alecthomas/assert/v2 v2.0.0-alpha3 h1:pcHeMvQ3OMstAWgaeaXIAL8uzB9xMm2zlxt+/4ml8lk=
@@ -98,9 +102,13 @@ github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96 h1:QAVZ3pN/J4/Uzi
 github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96/go.mod h1:Wa6n8cYIdaG35x15aH3Zy6d03f7P728QfdcDeD/IEOs=
 github.com/anacrolix/utp v0.1.0 h1:FOpQOmIwYsnENnz7tAGohA+r6iXpRjrq8ssKSre2Cp4=
 github.com/anacrolix/utp v0.1.0/go.mod h1:MDwc+vsGEq7RMw6lr2GKOEqjWny5hO5OZXRVNaBJ2Dk=
+github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
+github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
 github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
 github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
 github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
+github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
+github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
 github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
 github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
@@ -142,7 +150,6 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
 github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
 github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
-github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
 github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
 github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
 github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
@@ -158,6 +165,8 @@ github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWa
 github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
 github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
 github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
+github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
+github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
 github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
 github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
 github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
@@ -229,7 +238,6 @@ github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3a
 github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
 github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
-github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
@@ -358,13 +366,12 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
 github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
 github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
+github.com/lmittmann/tint v1.0.4 h1:LeYihpJ9hyGvE0w+K2okPTGUdVLfng1+nDNVR4vWISc=
+github.com/lmittmann/tint v1.0.4/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
-github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
-github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
-github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
-github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
@@ -500,9 +507,6 @@ github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDN
 github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
 github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 h1:Lt9DzQALzHoDwMBGJ6v8ObDPR0dzr2a6sXTB1Fq7IHs=
 github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA=
-github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
-github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
-github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
 github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -510,6 +514,8 @@ github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0
 github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8=
 github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
 github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
+github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
+github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
 github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c h1:aqg5Vm5dwtvL+YgDpBcK1ITf3o96N/K7/wsRXQnUTEs=
 github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c/go.mod h1:owqhoLW1qZoYLZzLnBw+QkPP9WZnjlSWihhxAJC1+/M=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
@@ -518,6 +524,8 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1
 github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
 github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs=
+github.com/sosodev/duration v1.1.0 h1:kQcaiGbJaIsRqgQy7VGlZrVw1giWO+lDoX3MCPnpVO4=
+github.com/sosodev/duration v1.1.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
 github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
 github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
 github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
@@ -558,14 +566,12 @@ github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
 github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 github.com/urfave/cli/v2 v2.27.0 h1:uNs1K8JwTFL84X68j5Fjny6hfANh9nTlJ6dRtZAFAHY=
 github.com/urfave/cli/v2 v2.27.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
+github.com/vektah/gqlparser/v2 v2.5.11 h1:JJxLtXIoN7+3x6MBdtIP59TP1RANnY7pXOaDnADQSf8=
+github.com/vektah/gqlparser/v2 v2.5.11/go.mod h1:1rCcfwB2ekJofmluGWXMSEnPMZgbxzwj6FaZ/4OT8Cc=
 github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
 github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
-github.com/willscott/go-nfs v0.0.2-0.20240102104133-9c599ee601d3 h1:ObGKsnV2OfjSlp0sCJwV0toiQl2eoqlFwrln1W5aBPg=
-github.com/willscott/go-nfs v0.0.2-0.20240102104133-9c599ee601d3/go.mod h1:+7+CzZfrWAP2Ff9h/6MhCMrjmitC21Yxt7nF/erAHNM=
 github.com/willscott/go-nfs v0.0.2 h1:BaBp1CpGDMooCT6bCgX6h6ZwgPcTMST4yToYZ9byee0=
 github.com/willscott/go-nfs v0.0.2/go.mod h1:SvullWeHxr/924WQNbUaZqtluBt2vuZ61g6yAV+xj7w=
-github.com/willscott/go-nfs-client v0.0.0-20200605172546-271fa9065b33 h1:Wd8wdpRzPXskyHvZLyw7Wc1fp5oCE2mhBCj7bAiibUs=
-github.com/willscott/go-nfs-client v0.0.0-20200605172546-271fa9065b33/go.mod h1:cOUKSNty+RabZqKhm5yTJT5Vq/Fe83ZRWAJ5Kj8nRes=
 github.com/willscott/go-nfs-client v0.0.0-20240104095149-b44639837b00 h1:U0DnHRZFzoIV1oFEZczg5XyPut9yxk9jjtax/9Bxr/o=
 github.com/willscott/go-nfs-client v0.0.0-20240104095149-b44639837b00/go.mod h1:Tq++Lr/FgiS3X48q5FETemXiSLGuYMQT2sPjYNPJSwA=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
@@ -574,7 +580,6 @@ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsr
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-github.com/zema1/go-nfs-client v0.0.0-20200604081958-0cf942f0e0fe/go.mod h1:im3CVJ32XM3+E+2RhY0sa5IVJVQehUrX0oE1wX4xOwU=
 go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
 go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
 go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
@@ -607,8 +612,8 @@ golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0
 golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
-golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
-golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
+golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -638,6 +643,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
+golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -689,8 +696,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
-golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
+golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -732,7 +739,6 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -740,9 +746,6 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
-golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
 golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -764,8 +767,8 @@ golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U=
-golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
+golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -828,8 +831,6 @@ google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvx
 google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
 google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
 google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w=
-google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
 google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
@@ -838,8 +839,6 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac
 google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
-google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -859,14 +858,13 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
-gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
-gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/graphql/mutation.graphql b/graphql/mutation.graphql
new file mode 100644
index 0000000..2d95198
--- /dev/null
+++ b/graphql/mutation.graphql
@@ -0,0 +1,10 @@
+type Mutation {
+    validateTorrents(filter: TorrentFilter!): Boolean!
+    cleanupTorrents(files: Boolean, dryRun: Boolean!): Int!
+}
+
+input TorrentFilter @oneOf {
+    everything: Boolean
+    infohash: String
+    # pathGlob: String!
+}
\ No newline at end of file
diff --git a/graphql/query.graphql b/graphql/query.graphql
new file mode 100644
index 0000000..95b7bab
--- /dev/null
+++ b/graphql/query.graphql
@@ -0,0 +1,43 @@
+type Query {
+  torrents(filter: TorrentsFilter, pagination: Pagination): [Torrent!]!
+}
+
+input TorrentsFilter {
+  name: StringFilter
+  bytesCompleted: IntFilter
+  bytesMissing: IntFilter
+
+  peersCount: IntFilter
+}
+
+input Pagination {
+  offset: Int!
+  limit: Int!
+}
+
+input StringFilter @oneOf {
+  eq: String
+  substr: String
+  in: [String!]
+}
+
+input IntFilter @oneOf {
+  eq: Int
+  gt: Int
+  lt: Int
+  gte: Int
+  lte: Int
+  in: [Int!]
+}
+
+input DateTimeFilter @oneOf {
+  eq: DateTime
+  gt: DateTime
+  lt: DateTime
+  gte: DateTime
+  lte: DateTime
+}
+
+input BooleanFilter @oneOf {
+  eq: Boolean
+}
\ No newline at end of file
diff --git a/graphql/schema.graphql b/graphql/schema.graphql
new file mode 100644
index 0000000..7942192
--- /dev/null
+++ b/graphql/schema.graphql
@@ -0,0 +1,9 @@
+directive @oneOf on INPUT_OBJECT | FIELD_DEFINITION
+
+scalar DateTime
+
+type Schema {
+  query: Query
+  mutation: Mutation
+}
+
diff --git a/graphql/types/torrent.graphql b/graphql/types/torrent.graphql
new file mode 100644
index 0000000..10e95f6
--- /dev/null
+++ b/graphql/types/torrent.graphql
@@ -0,0 +1,24 @@
+type Torrent {
+  name: String!
+  infohash: String!
+  bytesCompleted: Int!
+  torrentFilePath: String!
+  bytesMissing: Int!
+  files: [TorrentFile!]!
+  excludedFiles: [TorrentFile!]!
+  peers: [TorrentPeer!]!
+}
+
+type TorrentFile {
+  filename: String!
+  size: Int!
+  bytesCompleted: Int!
+}
+
+type TorrentPeer {
+  ip: String!
+  downloadRate: Float!
+  discovery: String!
+  port: Int!
+  clientName: String!
+}
\ No newline at end of file
diff --git a/src/delivery/graphql/generated.go b/src/delivery/graphql/generated.go
new file mode 100644
index 0000000..e91e331
--- /dev/null
+++ b/src/delivery/graphql/generated.go
@@ -0,0 +1,5741 @@
+// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
+
+package graph
+
+import (
+	"bytes"
+	"context"
+	"errors"
+	"fmt"
+	"strconv"
+	"sync"
+	"sync/atomic"
+	"time"
+
+	"git.kmsign.ru/royalcat/tstor/src/delivery/graphql/model"
+	"github.com/99designs/gqlgen/graphql"
+	"github.com/99designs/gqlgen/graphql/introspection"
+	gqlparser "github.com/vektah/gqlparser/v2"
+	"github.com/vektah/gqlparser/v2/ast"
+)
+
+// region    ************************** generated!.gotpl **************************
+
+// NewExecutableSchema creates an ExecutableSchema from the ResolverRoot interface.
+func NewExecutableSchema(cfg Config) graphql.ExecutableSchema {
+	return &executableSchema{
+		schema:     cfg.Schema,
+		resolvers:  cfg.Resolvers,
+		directives: cfg.Directives,
+		complexity: cfg.Complexity,
+	}
+}
+
+type Config struct {
+	Schema     *ast.Schema
+	Resolvers  ResolverRoot
+	Directives DirectiveRoot
+	Complexity ComplexityRoot
+}
+
+type ResolverRoot interface {
+	Mutation() MutationResolver
+	Query() QueryResolver
+	Torrent() TorrentResolver
+}
+
+type DirectiveRoot struct {
+	OneOf func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error)
+}
+
+type ComplexityRoot struct {
+	Mutation struct {
+		CleanupTorrents  func(childComplexity int, files *bool, dryRun bool) int
+		ValidateTorrents func(childComplexity int, filter model.TorrentFilter) int
+	}
+
+	Query struct {
+		Torrents func(childComplexity int, filter *model.TorrentsFilter, pagination *model.Pagination) int
+	}
+
+	Schema struct {
+		Mutation func(childComplexity int) int
+		Query    func(childComplexity int) int
+	}
+
+	Torrent struct {
+		BytesCompleted  func(childComplexity int) int
+		BytesMissing    func(childComplexity int) int
+		ExcludedFiles   func(childComplexity int) int
+		Files           func(childComplexity int) int
+		Infohash        func(childComplexity int) int
+		Name            func(childComplexity int) int
+		Peers           func(childComplexity int) int
+		TorrentFilePath func(childComplexity int) int
+	}
+
+	TorrentFile struct {
+		BytesCompleted func(childComplexity int) int
+		Filename       func(childComplexity int) int
+		Size           func(childComplexity int) int
+	}
+
+	TorrentPeer struct {
+		ClientName   func(childComplexity int) int
+		Discovery    func(childComplexity int) int
+		DownloadRate func(childComplexity int) int
+		IP           func(childComplexity int) int
+		Port         func(childComplexity int) int
+	}
+}
+
+type MutationResolver interface {
+	ValidateTorrents(ctx context.Context, filter model.TorrentFilter) (bool, error)
+	CleanupTorrents(ctx context.Context, files *bool, dryRun bool) (int64, error)
+}
+type QueryResolver interface {
+	Torrents(ctx context.Context, filter *model.TorrentsFilter, pagination *model.Pagination) ([]*model.Torrent, error)
+}
+type TorrentResolver interface {
+	Name(ctx context.Context, obj *model.Torrent) (string, error)
+
+	Files(ctx context.Context, obj *model.Torrent) ([]*model.TorrentFile, error)
+	ExcludedFiles(ctx context.Context, obj *model.Torrent) ([]*model.TorrentFile, error)
+	Peers(ctx context.Context, obj *model.Torrent) ([]*model.TorrentPeer, error)
+}
+
+type executableSchema struct {
+	schema     *ast.Schema
+	resolvers  ResolverRoot
+	directives DirectiveRoot
+	complexity ComplexityRoot
+}
+
+func (e *executableSchema) Schema() *ast.Schema {
+	if e.schema != nil {
+		return e.schema
+	}
+	return parsedSchema
+}
+
+func (e *executableSchema) Complexity(typeName, field string, childComplexity int, rawArgs map[string]interface{}) (int, bool) {
+	ec := executionContext{nil, e, 0, 0, nil}
+	_ = ec
+	switch typeName + "." + field {
+
+	case "Mutation.cleanupTorrents":
+		if e.complexity.Mutation.CleanupTorrents == nil {
+			break
+		}
+
+		args, err := ec.field_Mutation_cleanupTorrents_args(context.TODO(), rawArgs)
+		if err != nil {
+			return 0, false
+		}
+
+		return e.complexity.Mutation.CleanupTorrents(childComplexity, args["files"].(*bool), args["dryRun"].(bool)), true
+
+	case "Mutation.validateTorrents":
+		if e.complexity.Mutation.ValidateTorrents == nil {
+			break
+		}
+
+		args, err := ec.field_Mutation_validateTorrents_args(context.TODO(), rawArgs)
+		if err != nil {
+			return 0, false
+		}
+
+		return e.complexity.Mutation.ValidateTorrents(childComplexity, args["filter"].(model.TorrentFilter)), true
+
+	case "Query.torrents":
+		if e.complexity.Query.Torrents == nil {
+			break
+		}
+
+		args, err := ec.field_Query_torrents_args(context.TODO(), rawArgs)
+		if err != nil {
+			return 0, false
+		}
+
+		return e.complexity.Query.Torrents(childComplexity, args["filter"].(*model.TorrentsFilter), args["pagination"].(*model.Pagination)), true
+
+	case "Schema.mutation":
+		if e.complexity.Schema.Mutation == nil {
+			break
+		}
+
+		return e.complexity.Schema.Mutation(childComplexity), true
+
+	case "Schema.query":
+		if e.complexity.Schema.Query == nil {
+			break
+		}
+
+		return e.complexity.Schema.Query(childComplexity), true
+
+	case "Torrent.bytesCompleted":
+		if e.complexity.Torrent.BytesCompleted == nil {
+			break
+		}
+
+		return e.complexity.Torrent.BytesCompleted(childComplexity), true
+
+	case "Torrent.bytesMissing":
+		if e.complexity.Torrent.BytesMissing == nil {
+			break
+		}
+
+		return e.complexity.Torrent.BytesMissing(childComplexity), true
+
+	case "Torrent.excludedFiles":
+		if e.complexity.Torrent.ExcludedFiles == nil {
+			break
+		}
+
+		return e.complexity.Torrent.ExcludedFiles(childComplexity), true
+
+	case "Torrent.files":
+		if e.complexity.Torrent.Files == nil {
+			break
+		}
+
+		return e.complexity.Torrent.Files(childComplexity), true
+
+	case "Torrent.infohash":
+		if e.complexity.Torrent.Infohash == nil {
+			break
+		}
+
+		return e.complexity.Torrent.Infohash(childComplexity), true
+
+	case "Torrent.name":
+		if e.complexity.Torrent.Name == nil {
+			break
+		}
+
+		return e.complexity.Torrent.Name(childComplexity), true
+
+	case "Torrent.peers":
+		if e.complexity.Torrent.Peers == nil {
+			break
+		}
+
+		return e.complexity.Torrent.Peers(childComplexity), true
+
+	case "Torrent.torrentFilePath":
+		if e.complexity.Torrent.TorrentFilePath == nil {
+			break
+		}
+
+		return e.complexity.Torrent.TorrentFilePath(childComplexity), true
+
+	case "TorrentFile.bytesCompleted":
+		if e.complexity.TorrentFile.BytesCompleted == nil {
+			break
+		}
+
+		return e.complexity.TorrentFile.BytesCompleted(childComplexity), true
+
+	case "TorrentFile.filename":
+		if e.complexity.TorrentFile.Filename == nil {
+			break
+		}
+
+		return e.complexity.TorrentFile.Filename(childComplexity), true
+
+	case "TorrentFile.size":
+		if e.complexity.TorrentFile.Size == nil {
+			break
+		}
+
+		return e.complexity.TorrentFile.Size(childComplexity), true
+
+	case "TorrentPeer.clientName":
+		if e.complexity.TorrentPeer.ClientName == nil {
+			break
+		}
+
+		return e.complexity.TorrentPeer.ClientName(childComplexity), true
+
+	case "TorrentPeer.discovery":
+		if e.complexity.TorrentPeer.Discovery == nil {
+			break
+		}
+
+		return e.complexity.TorrentPeer.Discovery(childComplexity), true
+
+	case "TorrentPeer.downloadRate":
+		if e.complexity.TorrentPeer.DownloadRate == nil {
+			break
+		}
+
+		return e.complexity.TorrentPeer.DownloadRate(childComplexity), true
+
+	case "TorrentPeer.ip":
+		if e.complexity.TorrentPeer.IP == nil {
+			break
+		}
+
+		return e.complexity.TorrentPeer.IP(childComplexity), true
+
+	case "TorrentPeer.port":
+		if e.complexity.TorrentPeer.Port == nil {
+			break
+		}
+
+		return e.complexity.TorrentPeer.Port(childComplexity), true
+
+	}
+	return 0, false
+}
+
+func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
+	rc := graphql.GetOperationContext(ctx)
+	ec := executionContext{rc, e, 0, 0, make(chan graphql.DeferredResult)}
+	inputUnmarshalMap := graphql.BuildUnmarshalerMap(
+		ec.unmarshalInputBooleanFilter,
+		ec.unmarshalInputDateTimeFilter,
+		ec.unmarshalInputIntFilter,
+		ec.unmarshalInputPagination,
+		ec.unmarshalInputStringFilter,
+		ec.unmarshalInputTorrentFilter,
+		ec.unmarshalInputTorrentsFilter,
+	)
+	first := true
+
+	switch rc.Operation.Operation {
+	case ast.Query:
+		return func(ctx context.Context) *graphql.Response {
+			var response graphql.Response
+			var data graphql.Marshaler
+			if first {
+				first = false
+				ctx = graphql.WithUnmarshalerMap(ctx, inputUnmarshalMap)
+				data = ec._Query(ctx, rc.Operation.SelectionSet)
+			} else {
+				if atomic.LoadInt32(&ec.pendingDeferred) > 0 {
+					result := <-ec.deferredResults
+					atomic.AddInt32(&ec.pendingDeferred, -1)
+					data = result.Result
+					response.Path = result.Path
+					response.Label = result.Label
+					response.Errors = result.Errors
+				} else {
+					return nil
+				}
+			}
+			var buf bytes.Buffer
+			data.MarshalGQL(&buf)
+			response.Data = buf.Bytes()
+			if atomic.LoadInt32(&ec.deferred) > 0 {
+				hasNext := atomic.LoadInt32(&ec.pendingDeferred) > 0
+				response.HasNext = &hasNext
+			}
+
+			return &response
+		}
+	case ast.Mutation:
+		return func(ctx context.Context) *graphql.Response {
+			if !first {
+				return nil
+			}
+			first = false
+			ctx = graphql.WithUnmarshalerMap(ctx, inputUnmarshalMap)
+			data := ec._Mutation(ctx, rc.Operation.SelectionSet)
+			var buf bytes.Buffer
+			data.MarshalGQL(&buf)
+
+			return &graphql.Response{
+				Data: buf.Bytes(),
+			}
+		}
+
+	default:
+		return graphql.OneShot(graphql.ErrorResponse(ctx, "unsupported GraphQL operation"))
+	}
+}
+
+type executionContext struct {
+	*graphql.OperationContext
+	*executableSchema
+	deferred        int32
+	pendingDeferred int32
+	deferredResults chan graphql.DeferredResult
+}
+
+func (ec *executionContext) processDeferredGroup(dg graphql.DeferredGroup) {
+	atomic.AddInt32(&ec.pendingDeferred, 1)
+	go func() {
+		ctx := graphql.WithFreshResponseContext(dg.Context)
+		dg.FieldSet.Dispatch(ctx)
+		ds := graphql.DeferredResult{
+			Path:   dg.Path,
+			Label:  dg.Label,
+			Result: dg.FieldSet,
+			Errors: graphql.GetErrors(ctx),
+		}
+		// null fields should bubble up
+		if dg.FieldSet.Invalids > 0 {
+			ds.Result = graphql.Null
+		}
+		ec.deferredResults <- ds
+	}()
+}
+
+func (ec *executionContext) introspectSchema() (*introspection.Schema, error) {
+	if ec.DisableIntrospection {
+		return nil, errors.New("introspection disabled")
+	}
+	return introspection.WrapSchema(ec.Schema()), nil
+}
+
+func (ec *executionContext) introspectType(name string) (*introspection.Type, error) {
+	if ec.DisableIntrospection {
+		return nil, errors.New("introspection disabled")
+	}
+	return introspection.WrapTypeFromDef(ec.Schema(), ec.Schema().Types[name]), nil
+}
+
+var sources = []*ast.Source{
+	{Name: "../../../graphql/mutation.graphql", Input: `type Mutation {
+    validateTorrents(filter: TorrentFilter!): Boolean!
+    cleanupTorrents(files: Boolean, dryRun: Boolean!): Int!
+}
+
+input TorrentFilter @oneOf {
+    everything: Boolean
+    infohash: String
+    # pathGlob: String!
+}`, BuiltIn: false},
+	{Name: "../../../graphql/query.graphql", Input: `type Query {
+  torrents(filter: TorrentsFilter, pagination: Pagination): [Torrent!]!
+}
+
+input TorrentsFilter {
+  name: StringFilter
+  bytesCompleted: IntFilter
+  bytesMissing: IntFilter
+
+  peersCount: IntFilter
+}
+
+input Pagination {
+  offset: Int!
+  limit: Int!
+}
+
+input StringFilter @oneOf {
+  eq: String
+  substr: String
+  in: [String!]
+}
+
+input IntFilter @oneOf {
+  eq: Int
+  gt: Int
+  lt: Int
+  gte: Int
+  lte: Int
+  in: [Int!]
+}
+
+input DateTimeFilter @oneOf {
+  eq: DateTime
+  gt: DateTime
+  lt: DateTime
+  gte: DateTime
+  lte: DateTime
+}
+
+input BooleanFilter @oneOf {
+  eq: Boolean
+}`, BuiltIn: false},
+	{Name: "../../../graphql/schema.graphql", Input: `directive @oneOf on INPUT_OBJECT | FIELD_DEFINITION
+
+scalar DateTime
+
+type Schema {
+  query: Query
+  mutation: Mutation
+}
+
+`, BuiltIn: false},
+	{Name: "../../../graphql/types/torrent.graphql", Input: `type Torrent {
+  name: String!
+  infohash: String!
+  bytesCompleted: Int!
+  torrentFilePath: String!
+  bytesMissing: Int!
+  files: [TorrentFile!]!
+  excludedFiles: [TorrentFile!]!
+  peers: [TorrentPeer!]!
+}
+
+type TorrentFile {
+  filename: String!
+  size: Int!
+  bytesCompleted: Int!
+}
+
+type TorrentPeer {
+  ip: String!
+  downloadRate: Float!
+  discovery: String!
+  port: Int!
+  clientName: String!
+}`, BuiltIn: false},
+}
+var parsedSchema = gqlparser.MustLoadSchema(sources...)
+
+// endregion ************************** generated!.gotpl **************************
+
+// region    ***************************** args.gotpl *****************************
+
+func (ec *executionContext) field_Mutation_cleanupTorrents_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
+	var err error
+	args := map[string]interface{}{}
+	var arg0 *bool
+	if tmp, ok := rawArgs["files"]; ok {
+		ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("files"))
+		arg0, err = ec.unmarshalOBoolean2ᚖbool(ctx, tmp)
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["files"] = arg0
+	var arg1 bool
+	if tmp, ok := rawArgs["dryRun"]; ok {
+		ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("dryRun"))
+		arg1, err = ec.unmarshalNBoolean2bool(ctx, tmp)
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["dryRun"] = arg1
+	return args, nil
+}
+
+func (ec *executionContext) field_Mutation_validateTorrents_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
+	var err error
+	args := map[string]interface{}{}
+	var arg0 model.TorrentFilter
+	if tmp, ok := rawArgs["filter"]; ok {
+		ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("filter"))
+		arg0, err = ec.unmarshalNTorrentFilter2gitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentFilter(ctx, tmp)
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["filter"] = arg0
+	return args, nil
+}
+
+func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
+	var err error
+	args := map[string]interface{}{}
+	var arg0 string
+	if tmp, ok := rawArgs["name"]; ok {
+		ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name"))
+		arg0, err = ec.unmarshalNString2string(ctx, tmp)
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["name"] = arg0
+	return args, nil
+}
+
+func (ec *executionContext) field_Query_torrents_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
+	var err error
+	args := map[string]interface{}{}
+	var arg0 *model.TorrentsFilter
+	if tmp, ok := rawArgs["filter"]; ok {
+		ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("filter"))
+		arg0, err = ec.unmarshalOTorrentsFilter2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentsFilter(ctx, tmp)
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["filter"] = arg0
+	var arg1 *model.Pagination
+	if tmp, ok := rawArgs["pagination"]; ok {
+		ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("pagination"))
+		arg1, err = ec.unmarshalOPagination2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐPagination(ctx, tmp)
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["pagination"] = arg1
+	return args, nil
+}
+
+func (ec *executionContext) field___Type_enumValues_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
+	var err error
+	args := map[string]interface{}{}
+	var arg0 bool
+	if tmp, ok := rawArgs["includeDeprecated"]; ok {
+		ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("includeDeprecated"))
+		arg0, err = ec.unmarshalOBoolean2bool(ctx, tmp)
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["includeDeprecated"] = arg0
+	return args, nil
+}
+
+func (ec *executionContext) field___Type_fields_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
+	var err error
+	args := map[string]interface{}{}
+	var arg0 bool
+	if tmp, ok := rawArgs["includeDeprecated"]; ok {
+		ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("includeDeprecated"))
+		arg0, err = ec.unmarshalOBoolean2bool(ctx, tmp)
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["includeDeprecated"] = arg0
+	return args, nil
+}
+
+// endregion ***************************** args.gotpl *****************************
+
+// region    ************************** directives.gotpl **************************
+
+// endregion ************************** directives.gotpl **************************
+
+// region    **************************** field.gotpl *****************************
+
+func (ec *executionContext) _Mutation_validateTorrents(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_Mutation_validateTorrents(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return ec.resolvers.Mutation().ValidateTorrents(rctx, fc.Args["filter"].(model.TorrentFilter))
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(bool)
+	fc.Result = res
+	return ec.marshalNBoolean2bool(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Mutation_validateTorrents(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "Mutation",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: true,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Boolean does not have child fields")
+		},
+	}
+	defer func() {
+		if r := recover(); r != nil {
+			err = ec.Recover(ctx, r)
+			ec.Error(ctx, err)
+		}
+	}()
+	ctx = graphql.WithFieldContext(ctx, fc)
+	if fc.Args, err = ec.field_Mutation_validateTorrents_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
+		ec.Error(ctx, err)
+		return fc, err
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _Mutation_cleanupTorrents(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_Mutation_cleanupTorrents(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return ec.resolvers.Mutation().CleanupTorrents(rctx, fc.Args["files"].(*bool), fc.Args["dryRun"].(bool))
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(int64)
+	fc.Result = res
+	return ec.marshalNInt2int64(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Mutation_cleanupTorrents(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "Mutation",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: true,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Int does not have child fields")
+		},
+	}
+	defer func() {
+		if r := recover(); r != nil {
+			err = ec.Recover(ctx, r)
+			ec.Error(ctx, err)
+		}
+	}()
+	ctx = graphql.WithFieldContext(ctx, fc)
+	if fc.Args, err = ec.field_Mutation_cleanupTorrents_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
+		ec.Error(ctx, err)
+		return fc, err
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _Query_torrents(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_Query_torrents(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return ec.resolvers.Query().Torrents(rctx, fc.Args["filter"].(*model.TorrentsFilter), fc.Args["pagination"].(*model.Pagination))
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.([]*model.Torrent)
+	fc.Result = res
+	return ec.marshalNTorrent2ᚕᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentᚄ(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Query_torrents(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "Query",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: true,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			switch field.Name {
+			case "name":
+				return ec.fieldContext_Torrent_name(ctx, field)
+			case "infohash":
+				return ec.fieldContext_Torrent_infohash(ctx, field)
+			case "bytesCompleted":
+				return ec.fieldContext_Torrent_bytesCompleted(ctx, field)
+			case "torrentFilePath":
+				return ec.fieldContext_Torrent_torrentFilePath(ctx, field)
+			case "bytesMissing":
+				return ec.fieldContext_Torrent_bytesMissing(ctx, field)
+			case "files":
+				return ec.fieldContext_Torrent_files(ctx, field)
+			case "excludedFiles":
+				return ec.fieldContext_Torrent_excludedFiles(ctx, field)
+			case "peers":
+				return ec.fieldContext_Torrent_peers(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type Torrent", field.Name)
+		},
+	}
+	defer func() {
+		if r := recover(); r != nil {
+			err = ec.Recover(ctx, r)
+			ec.Error(ctx, err)
+		}
+	}()
+	ctx = graphql.WithFieldContext(ctx, fc)
+	if fc.Args, err = ec.field_Query_torrents_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
+		ec.Error(ctx, err)
+		return fc, err
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_Query___type(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return ec.introspectType(fc.Args["name"].(string))
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(*introspection.Type)
+	fc.Result = res
+	return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Query___type(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "Query",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			switch field.Name {
+			case "kind":
+				return ec.fieldContext___Type_kind(ctx, field)
+			case "name":
+				return ec.fieldContext___Type_name(ctx, field)
+			case "description":
+				return ec.fieldContext___Type_description(ctx, field)
+			case "fields":
+				return ec.fieldContext___Type_fields(ctx, field)
+			case "interfaces":
+				return ec.fieldContext___Type_interfaces(ctx, field)
+			case "possibleTypes":
+				return ec.fieldContext___Type_possibleTypes(ctx, field)
+			case "enumValues":
+				return ec.fieldContext___Type_enumValues(ctx, field)
+			case "inputFields":
+				return ec.fieldContext___Type_inputFields(ctx, field)
+			case "ofType":
+				return ec.fieldContext___Type_ofType(ctx, field)
+			case "specifiedByURL":
+				return ec.fieldContext___Type_specifiedByURL(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name)
+		},
+	}
+	defer func() {
+		if r := recover(); r != nil {
+			err = ec.Recover(ctx, r)
+			ec.Error(ctx, err)
+		}
+	}()
+	ctx = graphql.WithFieldContext(ctx, fc)
+	if fc.Args, err = ec.field_Query___type_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
+		ec.Error(ctx, err)
+		return fc, err
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _Query___schema(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_Query___schema(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return ec.introspectSchema()
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(*introspection.Schema)
+	fc.Result = res
+	return ec.marshalO__Schema2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐSchema(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Query___schema(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "Query",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			switch field.Name {
+			case "description":
+				return ec.fieldContext___Schema_description(ctx, field)
+			case "types":
+				return ec.fieldContext___Schema_types(ctx, field)
+			case "queryType":
+				return ec.fieldContext___Schema_queryType(ctx, field)
+			case "mutationType":
+				return ec.fieldContext___Schema_mutationType(ctx, field)
+			case "subscriptionType":
+				return ec.fieldContext___Schema_subscriptionType(ctx, field)
+			case "directives":
+				return ec.fieldContext___Schema_directives(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type __Schema", field.Name)
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _Schema_query(ctx context.Context, field graphql.CollectedField, obj *model.Schema) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_Schema_query(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	res := &model.Query{}
+	fc.Result = res
+	return ec.marshalOQuery2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐQuery(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Schema_query(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "Schema",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			switch field.Name {
+			case "torrents":
+				return ec.fieldContext_Query_torrents(ctx, field)
+			case "__schema":
+				return ec.fieldContext_Query___schema(ctx, field)
+			case "__type":
+				return ec.fieldContext_Query___type(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type Query", field.Name)
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _Schema_mutation(ctx context.Context, field graphql.CollectedField, obj *model.Schema) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_Schema_mutation(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	res := &model.Mutation{}
+	fc.Result = res
+	return ec.marshalOMutation2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐMutation(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Schema_mutation(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "Schema",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			switch field.Name {
+			case "validateTorrents":
+				return ec.fieldContext_Mutation_validateTorrents(ctx, field)
+			case "cleanupTorrents":
+				return ec.fieldContext_Mutation_cleanupTorrents(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type Mutation", field.Name)
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _Torrent_name(ctx context.Context, field graphql.CollectedField, obj *model.Torrent) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_Torrent_name(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return ec.resolvers.Torrent().Name(rctx, obj)
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(string)
+	fc.Result = res
+	return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Torrent_name(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "Torrent",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: true,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type String does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _Torrent_infohash(ctx context.Context, field graphql.CollectedField, obj *model.Torrent) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_Torrent_infohash(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Infohash, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(string)
+	fc.Result = res
+	return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Torrent_infohash(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "Torrent",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type String does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _Torrent_bytesCompleted(ctx context.Context, field graphql.CollectedField, obj *model.Torrent) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_Torrent_bytesCompleted(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.BytesCompleted, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(int64)
+	fc.Result = res
+	return ec.marshalNInt2int64(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Torrent_bytesCompleted(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "Torrent",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Int does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _Torrent_torrentFilePath(ctx context.Context, field graphql.CollectedField, obj *model.Torrent) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_Torrent_torrentFilePath(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.TorrentFilePath, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(string)
+	fc.Result = res
+	return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Torrent_torrentFilePath(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "Torrent",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type String does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _Torrent_bytesMissing(ctx context.Context, field graphql.CollectedField, obj *model.Torrent) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_Torrent_bytesMissing(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.BytesMissing, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(int64)
+	fc.Result = res
+	return ec.marshalNInt2int64(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Torrent_bytesMissing(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "Torrent",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Int does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _Torrent_files(ctx context.Context, field graphql.CollectedField, obj *model.Torrent) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_Torrent_files(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return ec.resolvers.Torrent().Files(rctx, obj)
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.([]*model.TorrentFile)
+	fc.Result = res
+	return ec.marshalNTorrentFile2ᚕᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentFileᚄ(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Torrent_files(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "Torrent",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: true,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			switch field.Name {
+			case "filename":
+				return ec.fieldContext_TorrentFile_filename(ctx, field)
+			case "size":
+				return ec.fieldContext_TorrentFile_size(ctx, field)
+			case "bytesCompleted":
+				return ec.fieldContext_TorrentFile_bytesCompleted(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type TorrentFile", field.Name)
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _Torrent_excludedFiles(ctx context.Context, field graphql.CollectedField, obj *model.Torrent) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_Torrent_excludedFiles(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return ec.resolvers.Torrent().ExcludedFiles(rctx, obj)
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.([]*model.TorrentFile)
+	fc.Result = res
+	return ec.marshalNTorrentFile2ᚕᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentFileᚄ(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Torrent_excludedFiles(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "Torrent",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: true,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			switch field.Name {
+			case "filename":
+				return ec.fieldContext_TorrentFile_filename(ctx, field)
+			case "size":
+				return ec.fieldContext_TorrentFile_size(ctx, field)
+			case "bytesCompleted":
+				return ec.fieldContext_TorrentFile_bytesCompleted(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type TorrentFile", field.Name)
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _Torrent_peers(ctx context.Context, field graphql.CollectedField, obj *model.Torrent) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_Torrent_peers(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return ec.resolvers.Torrent().Peers(rctx, obj)
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.([]*model.TorrentPeer)
+	fc.Result = res
+	return ec.marshalNTorrentPeer2ᚕᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentPeerᚄ(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Torrent_peers(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "Torrent",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: true,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			switch field.Name {
+			case "ip":
+				return ec.fieldContext_TorrentPeer_ip(ctx, field)
+			case "downloadRate":
+				return ec.fieldContext_TorrentPeer_downloadRate(ctx, field)
+			case "discovery":
+				return ec.fieldContext_TorrentPeer_discovery(ctx, field)
+			case "port":
+				return ec.fieldContext_TorrentPeer_port(ctx, field)
+			case "clientName":
+				return ec.fieldContext_TorrentPeer_clientName(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type TorrentPeer", field.Name)
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _TorrentFile_filename(ctx context.Context, field graphql.CollectedField, obj *model.TorrentFile) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentFile_filename(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Filename, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(string)
+	fc.Result = res
+	return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_TorrentFile_filename(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "TorrentFile",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type String does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _TorrentFile_size(ctx context.Context, field graphql.CollectedField, obj *model.TorrentFile) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentFile_size(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Size, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(int64)
+	fc.Result = res
+	return ec.marshalNInt2int64(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_TorrentFile_size(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "TorrentFile",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Int does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _TorrentFile_bytesCompleted(ctx context.Context, field graphql.CollectedField, obj *model.TorrentFile) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentFile_bytesCompleted(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.BytesCompleted, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(int64)
+	fc.Result = res
+	return ec.marshalNInt2int64(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_TorrentFile_bytesCompleted(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "TorrentFile",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Int does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _TorrentPeer_ip(ctx context.Context, field graphql.CollectedField, obj *model.TorrentPeer) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentPeer_ip(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.IP, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(string)
+	fc.Result = res
+	return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_TorrentPeer_ip(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "TorrentPeer",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type String does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _TorrentPeer_downloadRate(ctx context.Context, field graphql.CollectedField, obj *model.TorrentPeer) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentPeer_downloadRate(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.DownloadRate, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(float64)
+	fc.Result = res
+	return ec.marshalNFloat2float64(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_TorrentPeer_downloadRate(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "TorrentPeer",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Float does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _TorrentPeer_discovery(ctx context.Context, field graphql.CollectedField, obj *model.TorrentPeer) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentPeer_discovery(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Discovery, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(string)
+	fc.Result = res
+	return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_TorrentPeer_discovery(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "TorrentPeer",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type String does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _TorrentPeer_port(ctx context.Context, field graphql.CollectedField, obj *model.TorrentPeer) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentPeer_port(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Port, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(int64)
+	fc.Result = res
+	return ec.marshalNInt2int64(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_TorrentPeer_port(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "TorrentPeer",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Int does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _TorrentPeer_clientName(ctx context.Context, field graphql.CollectedField, obj *model.TorrentPeer) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentPeer_clientName(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.ClientName, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(string)
+	fc.Result = res
+	return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_TorrentPeer_clientName(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "TorrentPeer",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type String does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___Directive_name(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Name, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(string)
+	fc.Result = res
+	return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___Directive_name(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__Directive",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type String does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___Directive_description(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___Directive_description(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Description(), nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(*string)
+	fc.Result = res
+	return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___Directive_description(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__Directive",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type String does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___Directive_locations(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___Directive_locations(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Locations, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.([]string)
+	fc.Result = res
+	return ec.marshalN__DirectiveLocation2ᚕstringᚄ(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___Directive_locations(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__Directive",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type __DirectiveLocation does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___Directive_args(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___Directive_args(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Args, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.([]introspection.InputValue)
+	fc.Result = res
+	return ec.marshalN__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___Directive_args(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__Directive",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			switch field.Name {
+			case "name":
+				return ec.fieldContext___InputValue_name(ctx, field)
+			case "description":
+				return ec.fieldContext___InputValue_description(ctx, field)
+			case "type":
+				return ec.fieldContext___InputValue_type(ctx, field)
+			case "defaultValue":
+				return ec.fieldContext___InputValue_defaultValue(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type __InputValue", field.Name)
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___Directive_isRepeatable(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___Directive_isRepeatable(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.IsRepeatable, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(bool)
+	fc.Result = res
+	return ec.marshalNBoolean2bool(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___Directive_isRepeatable(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__Directive",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Boolean does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___EnumValue_name(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___EnumValue_name(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Name, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(string)
+	fc.Result = res
+	return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___EnumValue_name(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__EnumValue",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type String does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___EnumValue_description(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___EnumValue_description(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Description(), nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(*string)
+	fc.Result = res
+	return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___EnumValue_description(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__EnumValue",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type String does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___EnumValue_isDeprecated(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___EnumValue_isDeprecated(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.IsDeprecated(), nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(bool)
+	fc.Result = res
+	return ec.marshalNBoolean2bool(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___EnumValue_isDeprecated(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__EnumValue",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Boolean does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___EnumValue_deprecationReason(ctx context.Context, field graphql.CollectedField, obj *introspection.EnumValue) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___EnumValue_deprecationReason(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.DeprecationReason(), nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(*string)
+	fc.Result = res
+	return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___EnumValue_deprecationReason(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__EnumValue",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type String does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___Field_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___Field_name(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Name, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(string)
+	fc.Result = res
+	return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___Field_name(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__Field",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type String does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___Field_description(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___Field_description(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Description(), nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(*string)
+	fc.Result = res
+	return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___Field_description(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__Field",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type String does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___Field_args(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___Field_args(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Args, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.([]introspection.InputValue)
+	fc.Result = res
+	return ec.marshalN__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___Field_args(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__Field",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			switch field.Name {
+			case "name":
+				return ec.fieldContext___InputValue_name(ctx, field)
+			case "description":
+				return ec.fieldContext___InputValue_description(ctx, field)
+			case "type":
+				return ec.fieldContext___InputValue_type(ctx, field)
+			case "defaultValue":
+				return ec.fieldContext___InputValue_defaultValue(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type __InputValue", field.Name)
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___Field_type(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___Field_type(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Type, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(*introspection.Type)
+	fc.Result = res
+	return ec.marshalN__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___Field_type(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__Field",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			switch field.Name {
+			case "kind":
+				return ec.fieldContext___Type_kind(ctx, field)
+			case "name":
+				return ec.fieldContext___Type_name(ctx, field)
+			case "description":
+				return ec.fieldContext___Type_description(ctx, field)
+			case "fields":
+				return ec.fieldContext___Type_fields(ctx, field)
+			case "interfaces":
+				return ec.fieldContext___Type_interfaces(ctx, field)
+			case "possibleTypes":
+				return ec.fieldContext___Type_possibleTypes(ctx, field)
+			case "enumValues":
+				return ec.fieldContext___Type_enumValues(ctx, field)
+			case "inputFields":
+				return ec.fieldContext___Type_inputFields(ctx, field)
+			case "ofType":
+				return ec.fieldContext___Type_ofType(ctx, field)
+			case "specifiedByURL":
+				return ec.fieldContext___Type_specifiedByURL(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name)
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___Field_isDeprecated(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___Field_isDeprecated(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.IsDeprecated(), nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(bool)
+	fc.Result = res
+	return ec.marshalNBoolean2bool(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___Field_isDeprecated(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__Field",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Boolean does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___Field_deprecationReason(ctx context.Context, field graphql.CollectedField, obj *introspection.Field) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___Field_deprecationReason(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.DeprecationReason(), nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(*string)
+	fc.Result = res
+	return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___Field_deprecationReason(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__Field",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type String does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___InputValue_name(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___InputValue_name(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Name, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(string)
+	fc.Result = res
+	return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___InputValue_name(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__InputValue",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type String does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___InputValue_description(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___InputValue_description(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Description(), nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(*string)
+	fc.Result = res
+	return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___InputValue_description(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__InputValue",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type String does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___InputValue_type(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___InputValue_type(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Type, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(*introspection.Type)
+	fc.Result = res
+	return ec.marshalN__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___InputValue_type(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__InputValue",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			switch field.Name {
+			case "kind":
+				return ec.fieldContext___Type_kind(ctx, field)
+			case "name":
+				return ec.fieldContext___Type_name(ctx, field)
+			case "description":
+				return ec.fieldContext___Type_description(ctx, field)
+			case "fields":
+				return ec.fieldContext___Type_fields(ctx, field)
+			case "interfaces":
+				return ec.fieldContext___Type_interfaces(ctx, field)
+			case "possibleTypes":
+				return ec.fieldContext___Type_possibleTypes(ctx, field)
+			case "enumValues":
+				return ec.fieldContext___Type_enumValues(ctx, field)
+			case "inputFields":
+				return ec.fieldContext___Type_inputFields(ctx, field)
+			case "ofType":
+				return ec.fieldContext___Type_ofType(ctx, field)
+			case "specifiedByURL":
+				return ec.fieldContext___Type_specifiedByURL(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name)
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___InputValue_defaultValue(ctx context.Context, field graphql.CollectedField, obj *introspection.InputValue) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___InputValue_defaultValue(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.DefaultValue, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(*string)
+	fc.Result = res
+	return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___InputValue_defaultValue(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__InputValue",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type String does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___Schema_description(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___Schema_description(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Description(), nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(*string)
+	fc.Result = res
+	return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___Schema_description(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__Schema",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type String does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___Schema_types(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___Schema_types(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Types(), nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.([]introspection.Type)
+	fc.Result = res
+	return ec.marshalN__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐTypeᚄ(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___Schema_types(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__Schema",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			switch field.Name {
+			case "kind":
+				return ec.fieldContext___Type_kind(ctx, field)
+			case "name":
+				return ec.fieldContext___Type_name(ctx, field)
+			case "description":
+				return ec.fieldContext___Type_description(ctx, field)
+			case "fields":
+				return ec.fieldContext___Type_fields(ctx, field)
+			case "interfaces":
+				return ec.fieldContext___Type_interfaces(ctx, field)
+			case "possibleTypes":
+				return ec.fieldContext___Type_possibleTypes(ctx, field)
+			case "enumValues":
+				return ec.fieldContext___Type_enumValues(ctx, field)
+			case "inputFields":
+				return ec.fieldContext___Type_inputFields(ctx, field)
+			case "ofType":
+				return ec.fieldContext___Type_ofType(ctx, field)
+			case "specifiedByURL":
+				return ec.fieldContext___Type_specifiedByURL(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name)
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___Schema_queryType(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___Schema_queryType(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.QueryType(), nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(*introspection.Type)
+	fc.Result = res
+	return ec.marshalN__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___Schema_queryType(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__Schema",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			switch field.Name {
+			case "kind":
+				return ec.fieldContext___Type_kind(ctx, field)
+			case "name":
+				return ec.fieldContext___Type_name(ctx, field)
+			case "description":
+				return ec.fieldContext___Type_description(ctx, field)
+			case "fields":
+				return ec.fieldContext___Type_fields(ctx, field)
+			case "interfaces":
+				return ec.fieldContext___Type_interfaces(ctx, field)
+			case "possibleTypes":
+				return ec.fieldContext___Type_possibleTypes(ctx, field)
+			case "enumValues":
+				return ec.fieldContext___Type_enumValues(ctx, field)
+			case "inputFields":
+				return ec.fieldContext___Type_inputFields(ctx, field)
+			case "ofType":
+				return ec.fieldContext___Type_ofType(ctx, field)
+			case "specifiedByURL":
+				return ec.fieldContext___Type_specifiedByURL(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name)
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___Schema_mutationType(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___Schema_mutationType(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.MutationType(), nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(*introspection.Type)
+	fc.Result = res
+	return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___Schema_mutationType(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__Schema",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			switch field.Name {
+			case "kind":
+				return ec.fieldContext___Type_kind(ctx, field)
+			case "name":
+				return ec.fieldContext___Type_name(ctx, field)
+			case "description":
+				return ec.fieldContext___Type_description(ctx, field)
+			case "fields":
+				return ec.fieldContext___Type_fields(ctx, field)
+			case "interfaces":
+				return ec.fieldContext___Type_interfaces(ctx, field)
+			case "possibleTypes":
+				return ec.fieldContext___Type_possibleTypes(ctx, field)
+			case "enumValues":
+				return ec.fieldContext___Type_enumValues(ctx, field)
+			case "inputFields":
+				return ec.fieldContext___Type_inputFields(ctx, field)
+			case "ofType":
+				return ec.fieldContext___Type_ofType(ctx, field)
+			case "specifiedByURL":
+				return ec.fieldContext___Type_specifiedByURL(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name)
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___Schema_subscriptionType(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___Schema_subscriptionType(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.SubscriptionType(), nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(*introspection.Type)
+	fc.Result = res
+	return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___Schema_subscriptionType(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__Schema",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			switch field.Name {
+			case "kind":
+				return ec.fieldContext___Type_kind(ctx, field)
+			case "name":
+				return ec.fieldContext___Type_name(ctx, field)
+			case "description":
+				return ec.fieldContext___Type_description(ctx, field)
+			case "fields":
+				return ec.fieldContext___Type_fields(ctx, field)
+			case "interfaces":
+				return ec.fieldContext___Type_interfaces(ctx, field)
+			case "possibleTypes":
+				return ec.fieldContext___Type_possibleTypes(ctx, field)
+			case "enumValues":
+				return ec.fieldContext___Type_enumValues(ctx, field)
+			case "inputFields":
+				return ec.fieldContext___Type_inputFields(ctx, field)
+			case "ofType":
+				return ec.fieldContext___Type_ofType(ctx, field)
+			case "specifiedByURL":
+				return ec.fieldContext___Type_specifiedByURL(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name)
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___Schema_directives(ctx context.Context, field graphql.CollectedField, obj *introspection.Schema) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___Schema_directives(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Directives(), nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.([]introspection.Directive)
+	fc.Result = res
+	return ec.marshalN__Directive2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirectiveᚄ(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___Schema_directives(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__Schema",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			switch field.Name {
+			case "name":
+				return ec.fieldContext___Directive_name(ctx, field)
+			case "description":
+				return ec.fieldContext___Directive_description(ctx, field)
+			case "locations":
+				return ec.fieldContext___Directive_locations(ctx, field)
+			case "args":
+				return ec.fieldContext___Directive_args(ctx, field)
+			case "isRepeatable":
+				return ec.fieldContext___Directive_isRepeatable(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type __Directive", field.Name)
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___Type_kind(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___Type_kind(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Kind(), nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(string)
+	fc.Result = res
+	return ec.marshalN__TypeKind2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___Type_kind(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__Type",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type __TypeKind does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___Type_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___Type_name(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Name(), nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(*string)
+	fc.Result = res
+	return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___Type_name(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__Type",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type String does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___Type_description(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___Type_description(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Description(), nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(*string)
+	fc.Result = res
+	return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___Type_description(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__Type",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type String does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___Type_fields(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___Type_fields(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Fields(fc.Args["includeDeprecated"].(bool)), nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.([]introspection.Field)
+	fc.Result = res
+	return ec.marshalO__Field2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐFieldᚄ(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___Type_fields(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__Type",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			switch field.Name {
+			case "name":
+				return ec.fieldContext___Field_name(ctx, field)
+			case "description":
+				return ec.fieldContext___Field_description(ctx, field)
+			case "args":
+				return ec.fieldContext___Field_args(ctx, field)
+			case "type":
+				return ec.fieldContext___Field_type(ctx, field)
+			case "isDeprecated":
+				return ec.fieldContext___Field_isDeprecated(ctx, field)
+			case "deprecationReason":
+				return ec.fieldContext___Field_deprecationReason(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type __Field", field.Name)
+		},
+	}
+	defer func() {
+		if r := recover(); r != nil {
+			err = ec.Recover(ctx, r)
+			ec.Error(ctx, err)
+		}
+	}()
+	ctx = graphql.WithFieldContext(ctx, fc)
+	if fc.Args, err = ec.field___Type_fields_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
+		ec.Error(ctx, err)
+		return fc, err
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___Type_interfaces(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___Type_interfaces(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Interfaces(), nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.([]introspection.Type)
+	fc.Result = res
+	return ec.marshalO__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐTypeᚄ(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___Type_interfaces(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__Type",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			switch field.Name {
+			case "kind":
+				return ec.fieldContext___Type_kind(ctx, field)
+			case "name":
+				return ec.fieldContext___Type_name(ctx, field)
+			case "description":
+				return ec.fieldContext___Type_description(ctx, field)
+			case "fields":
+				return ec.fieldContext___Type_fields(ctx, field)
+			case "interfaces":
+				return ec.fieldContext___Type_interfaces(ctx, field)
+			case "possibleTypes":
+				return ec.fieldContext___Type_possibleTypes(ctx, field)
+			case "enumValues":
+				return ec.fieldContext___Type_enumValues(ctx, field)
+			case "inputFields":
+				return ec.fieldContext___Type_inputFields(ctx, field)
+			case "ofType":
+				return ec.fieldContext___Type_ofType(ctx, field)
+			case "specifiedByURL":
+				return ec.fieldContext___Type_specifiedByURL(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name)
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___Type_possibleTypes(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___Type_possibleTypes(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.PossibleTypes(), nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.([]introspection.Type)
+	fc.Result = res
+	return ec.marshalO__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐTypeᚄ(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___Type_possibleTypes(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__Type",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			switch field.Name {
+			case "kind":
+				return ec.fieldContext___Type_kind(ctx, field)
+			case "name":
+				return ec.fieldContext___Type_name(ctx, field)
+			case "description":
+				return ec.fieldContext___Type_description(ctx, field)
+			case "fields":
+				return ec.fieldContext___Type_fields(ctx, field)
+			case "interfaces":
+				return ec.fieldContext___Type_interfaces(ctx, field)
+			case "possibleTypes":
+				return ec.fieldContext___Type_possibleTypes(ctx, field)
+			case "enumValues":
+				return ec.fieldContext___Type_enumValues(ctx, field)
+			case "inputFields":
+				return ec.fieldContext___Type_inputFields(ctx, field)
+			case "ofType":
+				return ec.fieldContext___Type_ofType(ctx, field)
+			case "specifiedByURL":
+				return ec.fieldContext___Type_specifiedByURL(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name)
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___Type_enumValues(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___Type_enumValues(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.EnumValues(fc.Args["includeDeprecated"].(bool)), nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.([]introspection.EnumValue)
+	fc.Result = res
+	return ec.marshalO__EnumValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValueᚄ(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___Type_enumValues(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__Type",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			switch field.Name {
+			case "name":
+				return ec.fieldContext___EnumValue_name(ctx, field)
+			case "description":
+				return ec.fieldContext___EnumValue_description(ctx, field)
+			case "isDeprecated":
+				return ec.fieldContext___EnumValue_isDeprecated(ctx, field)
+			case "deprecationReason":
+				return ec.fieldContext___EnumValue_deprecationReason(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type __EnumValue", field.Name)
+		},
+	}
+	defer func() {
+		if r := recover(); r != nil {
+			err = ec.Recover(ctx, r)
+			ec.Error(ctx, err)
+		}
+	}()
+	ctx = graphql.WithFieldContext(ctx, fc)
+	if fc.Args, err = ec.field___Type_enumValues_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
+		ec.Error(ctx, err)
+		return fc, err
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___Type_inputFields(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___Type_inputFields(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.InputFields(), nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.([]introspection.InputValue)
+	fc.Result = res
+	return ec.marshalO__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___Type_inputFields(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__Type",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			switch field.Name {
+			case "name":
+				return ec.fieldContext___InputValue_name(ctx, field)
+			case "description":
+				return ec.fieldContext___InputValue_description(ctx, field)
+			case "type":
+				return ec.fieldContext___InputValue_type(ctx, field)
+			case "defaultValue":
+				return ec.fieldContext___InputValue_defaultValue(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type __InputValue", field.Name)
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___Type_ofType(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___Type_ofType(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.OfType(), nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(*introspection.Type)
+	fc.Result = res
+	return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___Type_ofType(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__Type",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			switch field.Name {
+			case "kind":
+				return ec.fieldContext___Type_kind(ctx, field)
+			case "name":
+				return ec.fieldContext___Type_name(ctx, field)
+			case "description":
+				return ec.fieldContext___Type_description(ctx, field)
+			case "fields":
+				return ec.fieldContext___Type_fields(ctx, field)
+			case "interfaces":
+				return ec.fieldContext___Type_interfaces(ctx, field)
+			case "possibleTypes":
+				return ec.fieldContext___Type_possibleTypes(ctx, field)
+			case "enumValues":
+				return ec.fieldContext___Type_enumValues(ctx, field)
+			case "inputFields":
+				return ec.fieldContext___Type_inputFields(ctx, field)
+			case "ofType":
+				return ec.fieldContext___Type_ofType(ctx, field)
+			case "specifiedByURL":
+				return ec.fieldContext___Type_specifiedByURL(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type __Type", field.Name)
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) ___Type_specifiedByURL(ctx context.Context, field graphql.CollectedField, obj *introspection.Type) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext___Type_specifiedByURL(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.SpecifiedByURL(), nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(*string)
+	fc.Result = res
+	return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext___Type_specifiedByURL(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "__Type",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type String does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+// endregion **************************** field.gotpl *****************************
+
+// region    **************************** input.gotpl *****************************
+
+func (ec *executionContext) unmarshalInputBooleanFilter(ctx context.Context, obj interface{}) (model.BooleanFilter, error) {
+	var it model.BooleanFilter
+	asMap := map[string]interface{}{}
+	for k, v := range obj.(map[string]interface{}) {
+		asMap[k] = v
+	}
+
+	fieldsInOrder := [...]string{"eq"}
+	for _, k := range fieldsInOrder {
+		v, ok := asMap[k]
+		if !ok {
+			continue
+		}
+		switch k {
+		case "eq":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("eq"))
+			directive0 := func(ctx context.Context) (interface{}, error) { return ec.unmarshalOBoolean2ᚖbool(ctx, v) }
+			directive1 := func(ctx context.Context) (interface{}, error) {
+				if ec.directives.OneOf == nil {
+					return nil, errors.New("directive oneOf is not implemented")
+				}
+				return ec.directives.OneOf(ctx, obj, directive0)
+			}
+
+			tmp, err := directive1(ctx)
+			if err != nil {
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+			if data, ok := tmp.(*bool); ok {
+				it.Eq = data
+			} else if tmp == nil {
+				it.Eq = nil
+			} else {
+				err := fmt.Errorf(`unexpected type %T from directive, should be *bool`, tmp)
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+		}
+	}
+
+	return it, nil
+}
+
+func (ec *executionContext) unmarshalInputDateTimeFilter(ctx context.Context, obj interface{}) (model.DateTimeFilter, error) {
+	var it model.DateTimeFilter
+	asMap := map[string]interface{}{}
+	for k, v := range obj.(map[string]interface{}) {
+		asMap[k] = v
+	}
+
+	fieldsInOrder := [...]string{"eq", "gt", "lt", "gte", "lte"}
+	for _, k := range fieldsInOrder {
+		v, ok := asMap[k]
+		if !ok {
+			continue
+		}
+		switch k {
+		case "eq":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("eq"))
+			directive0 := func(ctx context.Context) (interface{}, error) { return ec.unmarshalODateTime2ᚖtimeᚐTime(ctx, v) }
+			directive1 := func(ctx context.Context) (interface{}, error) {
+				if ec.directives.OneOf == nil {
+					return nil, errors.New("directive oneOf is not implemented")
+				}
+				return ec.directives.OneOf(ctx, obj, directive0)
+			}
+
+			tmp, err := directive1(ctx)
+			if err != nil {
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+			if data, ok := tmp.(*time.Time); ok {
+				it.Eq = data
+			} else if tmp == nil {
+				it.Eq = nil
+			} else {
+				err := fmt.Errorf(`unexpected type %T from directive, should be *time.Time`, tmp)
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+		case "gt":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("gt"))
+			directive0 := func(ctx context.Context) (interface{}, error) { return ec.unmarshalODateTime2ᚖtimeᚐTime(ctx, v) }
+			directive1 := func(ctx context.Context) (interface{}, error) {
+				if ec.directives.OneOf == nil {
+					return nil, errors.New("directive oneOf is not implemented")
+				}
+				return ec.directives.OneOf(ctx, obj, directive0)
+			}
+
+			tmp, err := directive1(ctx)
+			if err != nil {
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+			if data, ok := tmp.(*time.Time); ok {
+				it.Gt = data
+			} else if tmp == nil {
+				it.Gt = nil
+			} else {
+				err := fmt.Errorf(`unexpected type %T from directive, should be *time.Time`, tmp)
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+		case "lt":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("lt"))
+			directive0 := func(ctx context.Context) (interface{}, error) { return ec.unmarshalODateTime2ᚖtimeᚐTime(ctx, v) }
+			directive1 := func(ctx context.Context) (interface{}, error) {
+				if ec.directives.OneOf == nil {
+					return nil, errors.New("directive oneOf is not implemented")
+				}
+				return ec.directives.OneOf(ctx, obj, directive0)
+			}
+
+			tmp, err := directive1(ctx)
+			if err != nil {
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+			if data, ok := tmp.(*time.Time); ok {
+				it.Lt = data
+			} else if tmp == nil {
+				it.Lt = nil
+			} else {
+				err := fmt.Errorf(`unexpected type %T from directive, should be *time.Time`, tmp)
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+		case "gte":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("gte"))
+			directive0 := func(ctx context.Context) (interface{}, error) { return ec.unmarshalODateTime2ᚖtimeᚐTime(ctx, v) }
+			directive1 := func(ctx context.Context) (interface{}, error) {
+				if ec.directives.OneOf == nil {
+					return nil, errors.New("directive oneOf is not implemented")
+				}
+				return ec.directives.OneOf(ctx, obj, directive0)
+			}
+
+			tmp, err := directive1(ctx)
+			if err != nil {
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+			if data, ok := tmp.(*time.Time); ok {
+				it.Gte = data
+			} else if tmp == nil {
+				it.Gte = nil
+			} else {
+				err := fmt.Errorf(`unexpected type %T from directive, should be *time.Time`, tmp)
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+		case "lte":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("lte"))
+			directive0 := func(ctx context.Context) (interface{}, error) { return ec.unmarshalODateTime2ᚖtimeᚐTime(ctx, v) }
+			directive1 := func(ctx context.Context) (interface{}, error) {
+				if ec.directives.OneOf == nil {
+					return nil, errors.New("directive oneOf is not implemented")
+				}
+				return ec.directives.OneOf(ctx, obj, directive0)
+			}
+
+			tmp, err := directive1(ctx)
+			if err != nil {
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+			if data, ok := tmp.(*time.Time); ok {
+				it.Lte = data
+			} else if tmp == nil {
+				it.Lte = nil
+			} else {
+				err := fmt.Errorf(`unexpected type %T from directive, should be *time.Time`, tmp)
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+		}
+	}
+
+	return it, nil
+}
+
+func (ec *executionContext) unmarshalInputIntFilter(ctx context.Context, obj interface{}) (model.IntFilter, error) {
+	var it model.IntFilter
+	asMap := map[string]interface{}{}
+	for k, v := range obj.(map[string]interface{}) {
+		asMap[k] = v
+	}
+
+	fieldsInOrder := [...]string{"eq", "gt", "lt", "gte", "lte", "in"}
+	for _, k := range fieldsInOrder {
+		v, ok := asMap[k]
+		if !ok {
+			continue
+		}
+		switch k {
+		case "eq":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("eq"))
+			directive0 := func(ctx context.Context) (interface{}, error) { return ec.unmarshalOInt2ᚖint64(ctx, v) }
+			directive1 := func(ctx context.Context) (interface{}, error) {
+				if ec.directives.OneOf == nil {
+					return nil, errors.New("directive oneOf is not implemented")
+				}
+				return ec.directives.OneOf(ctx, obj, directive0)
+			}
+
+			tmp, err := directive1(ctx)
+			if err != nil {
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+			if data, ok := tmp.(*int64); ok {
+				it.Eq = data
+			} else if tmp == nil {
+				it.Eq = nil
+			} else {
+				err := fmt.Errorf(`unexpected type %T from directive, should be *int64`, tmp)
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+		case "gt":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("gt"))
+			directive0 := func(ctx context.Context) (interface{}, error) { return ec.unmarshalOInt2ᚖint64(ctx, v) }
+			directive1 := func(ctx context.Context) (interface{}, error) {
+				if ec.directives.OneOf == nil {
+					return nil, errors.New("directive oneOf is not implemented")
+				}
+				return ec.directives.OneOf(ctx, obj, directive0)
+			}
+
+			tmp, err := directive1(ctx)
+			if err != nil {
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+			if data, ok := tmp.(*int64); ok {
+				it.Gt = data
+			} else if tmp == nil {
+				it.Gt = nil
+			} else {
+				err := fmt.Errorf(`unexpected type %T from directive, should be *int64`, tmp)
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+		case "lt":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("lt"))
+			directive0 := func(ctx context.Context) (interface{}, error) { return ec.unmarshalOInt2ᚖint64(ctx, v) }
+			directive1 := func(ctx context.Context) (interface{}, error) {
+				if ec.directives.OneOf == nil {
+					return nil, errors.New("directive oneOf is not implemented")
+				}
+				return ec.directives.OneOf(ctx, obj, directive0)
+			}
+
+			tmp, err := directive1(ctx)
+			if err != nil {
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+			if data, ok := tmp.(*int64); ok {
+				it.Lt = data
+			} else if tmp == nil {
+				it.Lt = nil
+			} else {
+				err := fmt.Errorf(`unexpected type %T from directive, should be *int64`, tmp)
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+		case "gte":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("gte"))
+			directive0 := func(ctx context.Context) (interface{}, error) { return ec.unmarshalOInt2ᚖint64(ctx, v) }
+			directive1 := func(ctx context.Context) (interface{}, error) {
+				if ec.directives.OneOf == nil {
+					return nil, errors.New("directive oneOf is not implemented")
+				}
+				return ec.directives.OneOf(ctx, obj, directive0)
+			}
+
+			tmp, err := directive1(ctx)
+			if err != nil {
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+			if data, ok := tmp.(*int64); ok {
+				it.Gte = data
+			} else if tmp == nil {
+				it.Gte = nil
+			} else {
+				err := fmt.Errorf(`unexpected type %T from directive, should be *int64`, tmp)
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+		case "lte":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("lte"))
+			directive0 := func(ctx context.Context) (interface{}, error) { return ec.unmarshalOInt2ᚖint64(ctx, v) }
+			directive1 := func(ctx context.Context) (interface{}, error) {
+				if ec.directives.OneOf == nil {
+					return nil, errors.New("directive oneOf is not implemented")
+				}
+				return ec.directives.OneOf(ctx, obj, directive0)
+			}
+
+			tmp, err := directive1(ctx)
+			if err != nil {
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+			if data, ok := tmp.(*int64); ok {
+				it.Lte = data
+			} else if tmp == nil {
+				it.Lte = nil
+			} else {
+				err := fmt.Errorf(`unexpected type %T from directive, should be *int64`, tmp)
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+		case "in":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("in"))
+			directive0 := func(ctx context.Context) (interface{}, error) { return ec.unmarshalOInt2ᚕint64ᚄ(ctx, v) }
+			directive1 := func(ctx context.Context) (interface{}, error) {
+				if ec.directives.OneOf == nil {
+					return nil, errors.New("directive oneOf is not implemented")
+				}
+				return ec.directives.OneOf(ctx, obj, directive0)
+			}
+
+			tmp, err := directive1(ctx)
+			if err != nil {
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+			if data, ok := tmp.([]int64); ok {
+				it.In = data
+			} else if tmp == nil {
+				it.In = nil
+			} else {
+				err := fmt.Errorf(`unexpected type %T from directive, should be []int64`, tmp)
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+		}
+	}
+
+	return it, nil
+}
+
+func (ec *executionContext) unmarshalInputPagination(ctx context.Context, obj interface{}) (model.Pagination, error) {
+	var it model.Pagination
+	asMap := map[string]interface{}{}
+	for k, v := range obj.(map[string]interface{}) {
+		asMap[k] = v
+	}
+
+	fieldsInOrder := [...]string{"offset", "limit"}
+	for _, k := range fieldsInOrder {
+		v, ok := asMap[k]
+		if !ok {
+			continue
+		}
+		switch k {
+		case "offset":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("offset"))
+			data, err := ec.unmarshalNInt2int64(ctx, v)
+			if err != nil {
+				return it, err
+			}
+			it.Offset = data
+		case "limit":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("limit"))
+			data, err := ec.unmarshalNInt2int64(ctx, v)
+			if err != nil {
+				return it, err
+			}
+			it.Limit = data
+		}
+	}
+
+	return it, nil
+}
+
+func (ec *executionContext) unmarshalInputStringFilter(ctx context.Context, obj interface{}) (model.StringFilter, error) {
+	var it model.StringFilter
+	asMap := map[string]interface{}{}
+	for k, v := range obj.(map[string]interface{}) {
+		asMap[k] = v
+	}
+
+	fieldsInOrder := [...]string{"eq", "substr", "in"}
+	for _, k := range fieldsInOrder {
+		v, ok := asMap[k]
+		if !ok {
+			continue
+		}
+		switch k {
+		case "eq":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("eq"))
+			directive0 := func(ctx context.Context) (interface{}, error) { return ec.unmarshalOString2ᚖstring(ctx, v) }
+			directive1 := func(ctx context.Context) (interface{}, error) {
+				if ec.directives.OneOf == nil {
+					return nil, errors.New("directive oneOf is not implemented")
+				}
+				return ec.directives.OneOf(ctx, obj, directive0)
+			}
+
+			tmp, err := directive1(ctx)
+			if err != nil {
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+			if data, ok := tmp.(*string); ok {
+				it.Eq = data
+			} else if tmp == nil {
+				it.Eq = nil
+			} else {
+				err := fmt.Errorf(`unexpected type %T from directive, should be *string`, tmp)
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+		case "substr":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("substr"))
+			directive0 := func(ctx context.Context) (interface{}, error) { return ec.unmarshalOString2ᚖstring(ctx, v) }
+			directive1 := func(ctx context.Context) (interface{}, error) {
+				if ec.directives.OneOf == nil {
+					return nil, errors.New("directive oneOf is not implemented")
+				}
+				return ec.directives.OneOf(ctx, obj, directive0)
+			}
+
+			tmp, err := directive1(ctx)
+			if err != nil {
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+			if data, ok := tmp.(*string); ok {
+				it.Substr = data
+			} else if tmp == nil {
+				it.Substr = nil
+			} else {
+				err := fmt.Errorf(`unexpected type %T from directive, should be *string`, tmp)
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+		case "in":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("in"))
+			directive0 := func(ctx context.Context) (interface{}, error) { return ec.unmarshalOString2ᚕstringᚄ(ctx, v) }
+			directive1 := func(ctx context.Context) (interface{}, error) {
+				if ec.directives.OneOf == nil {
+					return nil, errors.New("directive oneOf is not implemented")
+				}
+				return ec.directives.OneOf(ctx, obj, directive0)
+			}
+
+			tmp, err := directive1(ctx)
+			if err != nil {
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+			if data, ok := tmp.([]string); ok {
+				it.In = data
+			} else if tmp == nil {
+				it.In = nil
+			} else {
+				err := fmt.Errorf(`unexpected type %T from directive, should be []string`, tmp)
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+		}
+	}
+
+	return it, nil
+}
+
+func (ec *executionContext) unmarshalInputTorrentFilter(ctx context.Context, obj interface{}) (model.TorrentFilter, error) {
+	var it model.TorrentFilter
+	asMap := map[string]interface{}{}
+	for k, v := range obj.(map[string]interface{}) {
+		asMap[k] = v
+	}
+
+	fieldsInOrder := [...]string{"everything", "infohash"}
+	for _, k := range fieldsInOrder {
+		v, ok := asMap[k]
+		if !ok {
+			continue
+		}
+		switch k {
+		case "everything":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("everything"))
+			directive0 := func(ctx context.Context) (interface{}, error) { return ec.unmarshalOBoolean2ᚖbool(ctx, v) }
+			directive1 := func(ctx context.Context) (interface{}, error) {
+				if ec.directives.OneOf == nil {
+					return nil, errors.New("directive oneOf is not implemented")
+				}
+				return ec.directives.OneOf(ctx, obj, directive0)
+			}
+
+			tmp, err := directive1(ctx)
+			if err != nil {
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+			if data, ok := tmp.(*bool); ok {
+				it.Everything = data
+			} else if tmp == nil {
+				it.Everything = nil
+			} else {
+				err := fmt.Errorf(`unexpected type %T from directive, should be *bool`, tmp)
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+		case "infohash":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("infohash"))
+			directive0 := func(ctx context.Context) (interface{}, error) { return ec.unmarshalOString2ᚖstring(ctx, v) }
+			directive1 := func(ctx context.Context) (interface{}, error) {
+				if ec.directives.OneOf == nil {
+					return nil, errors.New("directive oneOf is not implemented")
+				}
+				return ec.directives.OneOf(ctx, obj, directive0)
+			}
+
+			tmp, err := directive1(ctx)
+			if err != nil {
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+			if data, ok := tmp.(*string); ok {
+				it.Infohash = data
+			} else if tmp == nil {
+				it.Infohash = nil
+			} else {
+				err := fmt.Errorf(`unexpected type %T from directive, should be *string`, tmp)
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+		}
+	}
+
+	return it, nil
+}
+
+func (ec *executionContext) unmarshalInputTorrentsFilter(ctx context.Context, obj interface{}) (model.TorrentsFilter, error) {
+	var it model.TorrentsFilter
+	asMap := map[string]interface{}{}
+	for k, v := range obj.(map[string]interface{}) {
+		asMap[k] = v
+	}
+
+	fieldsInOrder := [...]string{"name", "bytesCompleted", "bytesMissing", "peersCount"}
+	for _, k := range fieldsInOrder {
+		v, ok := asMap[k]
+		if !ok {
+			continue
+		}
+		switch k {
+		case "name":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name"))
+			data, err := ec.unmarshalOStringFilter2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐStringFilter(ctx, v)
+			if err != nil {
+				return it, err
+			}
+			it.Name = data
+		case "bytesCompleted":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("bytesCompleted"))
+			data, err := ec.unmarshalOIntFilter2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐIntFilter(ctx, v)
+			if err != nil {
+				return it, err
+			}
+			it.BytesCompleted = data
+		case "bytesMissing":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("bytesMissing"))
+			data, err := ec.unmarshalOIntFilter2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐIntFilter(ctx, v)
+			if err != nil {
+				return it, err
+			}
+			it.BytesMissing = data
+		case "peersCount":
+			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("peersCount"))
+			data, err := ec.unmarshalOIntFilter2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐIntFilter(ctx, v)
+			if err != nil {
+				return it, err
+			}
+			it.PeersCount = data
+		}
+	}
+
+	return it, nil
+}
+
+// endregion **************************** input.gotpl *****************************
+
+// region    ************************** interface.gotpl ***************************
+
+// endregion ************************** interface.gotpl ***************************
+
+// region    **************************** object.gotpl ****************************
+
+var mutationImplementors = []string{"Mutation"}
+
+func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler {
+	fields := graphql.CollectFields(ec.OperationContext, sel, mutationImplementors)
+	ctx = graphql.WithFieldContext(ctx, &graphql.FieldContext{
+		Object: "Mutation",
+	})
+
+	out := graphql.NewFieldSet(fields)
+	deferred := make(map[string]*graphql.FieldSet)
+	for i, field := range fields {
+		innerCtx := graphql.WithRootFieldContext(ctx, &graphql.RootFieldContext{
+			Object: field.Name,
+			Field:  field,
+		})
+
+		switch field.Name {
+		case "__typename":
+			out.Values[i] = graphql.MarshalString("Mutation")
+		case "validateTorrents":
+			out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
+				return ec._Mutation_validateTorrents(ctx, field)
+			})
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "cleanupTorrents":
+			out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
+				return ec._Mutation_cleanupTorrents(ctx, field)
+			})
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		default:
+			panic("unknown field " + strconv.Quote(field.Name))
+		}
+	}
+	out.Dispatch(ctx)
+	if out.Invalids > 0 {
+		return graphql.Null
+	}
+
+	atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+	for label, dfs := range deferred {
+		ec.processDeferredGroup(graphql.DeferredGroup{
+			Label:    label,
+			Path:     graphql.GetPath(ctx),
+			FieldSet: dfs,
+			Context:  ctx,
+		})
+	}
+
+	return out
+}
+
+var queryImplementors = []string{"Query"}
+
+func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler {
+	fields := graphql.CollectFields(ec.OperationContext, sel, queryImplementors)
+	ctx = graphql.WithFieldContext(ctx, &graphql.FieldContext{
+		Object: "Query",
+	})
+
+	out := graphql.NewFieldSet(fields)
+	deferred := make(map[string]*graphql.FieldSet)
+	for i, field := range fields {
+		innerCtx := graphql.WithRootFieldContext(ctx, &graphql.RootFieldContext{
+			Object: field.Name,
+			Field:  field,
+		})
+
+		switch field.Name {
+		case "__typename":
+			out.Values[i] = graphql.MarshalString("Query")
+		case "torrents":
+			field := field
+
+			innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
+				defer func() {
+					if r := recover(); r != nil {
+						ec.Error(ctx, ec.Recover(ctx, r))
+					}
+				}()
+				res = ec._Query_torrents(ctx, field)
+				if res == graphql.Null {
+					atomic.AddUint32(&fs.Invalids, 1)
+				}
+				return res
+			}
+
+			rrm := func(ctx context.Context) graphql.Marshaler {
+				return ec.OperationContext.RootResolverMiddleware(ctx,
+					func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
+			}
+
+			out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) })
+		case "__type":
+			out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
+				return ec._Query___type(ctx, field)
+			})
+		case "__schema":
+			out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
+				return ec._Query___schema(ctx, field)
+			})
+		default:
+			panic("unknown field " + strconv.Quote(field.Name))
+		}
+	}
+	out.Dispatch(ctx)
+	if out.Invalids > 0 {
+		return graphql.Null
+	}
+
+	atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+	for label, dfs := range deferred {
+		ec.processDeferredGroup(graphql.DeferredGroup{
+			Label:    label,
+			Path:     graphql.GetPath(ctx),
+			FieldSet: dfs,
+			Context:  ctx,
+		})
+	}
+
+	return out
+}
+
+var schemaImplementors = []string{"Schema"}
+
+func (ec *executionContext) _Schema(ctx context.Context, sel ast.SelectionSet, obj *model.Schema) graphql.Marshaler {
+	fields := graphql.CollectFields(ec.OperationContext, sel, schemaImplementors)
+
+	out := graphql.NewFieldSet(fields)
+	deferred := make(map[string]*graphql.FieldSet)
+	for i, field := range fields {
+		switch field.Name {
+		case "__typename":
+			out.Values[i] = graphql.MarshalString("Schema")
+		case "query":
+			out.Values[i] = ec._Schema_query(ctx, field, obj)
+		case "mutation":
+			out.Values[i] = ec._Schema_mutation(ctx, field, obj)
+		default:
+			panic("unknown field " + strconv.Quote(field.Name))
+		}
+	}
+	out.Dispatch(ctx)
+	if out.Invalids > 0 {
+		return graphql.Null
+	}
+
+	atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+	for label, dfs := range deferred {
+		ec.processDeferredGroup(graphql.DeferredGroup{
+			Label:    label,
+			Path:     graphql.GetPath(ctx),
+			FieldSet: dfs,
+			Context:  ctx,
+		})
+	}
+
+	return out
+}
+
+var torrentImplementors = []string{"Torrent"}
+
+func (ec *executionContext) _Torrent(ctx context.Context, sel ast.SelectionSet, obj *model.Torrent) graphql.Marshaler {
+	fields := graphql.CollectFields(ec.OperationContext, sel, torrentImplementors)
+
+	out := graphql.NewFieldSet(fields)
+	deferred := make(map[string]*graphql.FieldSet)
+	for i, field := range fields {
+		switch field.Name {
+		case "__typename":
+			out.Values[i] = graphql.MarshalString("Torrent")
+		case "name":
+			field := field
+
+			innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
+				defer func() {
+					if r := recover(); r != nil {
+						ec.Error(ctx, ec.Recover(ctx, r))
+					}
+				}()
+				res = ec._Torrent_name(ctx, field, obj)
+				if res == graphql.Null {
+					atomic.AddUint32(&fs.Invalids, 1)
+				}
+				return res
+			}
+
+			if field.Deferrable != nil {
+				dfs, ok := deferred[field.Deferrable.Label]
+				di := 0
+				if ok {
+					dfs.AddField(field)
+					di = len(dfs.Values) - 1
+				} else {
+					dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
+					deferred[field.Deferrable.Label] = dfs
+				}
+				dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
+					return innerFunc(ctx, dfs)
+				})
+
+				// don't run the out.Concurrently() call below
+				out.Values[i] = graphql.Null
+				continue
+			}
+
+			out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
+		case "infohash":
+			out.Values[i] = ec._Torrent_infohash(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				atomic.AddUint32(&out.Invalids, 1)
+			}
+		case "bytesCompleted":
+			out.Values[i] = ec._Torrent_bytesCompleted(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				atomic.AddUint32(&out.Invalids, 1)
+			}
+		case "torrentFilePath":
+			out.Values[i] = ec._Torrent_torrentFilePath(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				atomic.AddUint32(&out.Invalids, 1)
+			}
+		case "bytesMissing":
+			out.Values[i] = ec._Torrent_bytesMissing(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				atomic.AddUint32(&out.Invalids, 1)
+			}
+		case "files":
+			field := field
+
+			innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
+				defer func() {
+					if r := recover(); r != nil {
+						ec.Error(ctx, ec.Recover(ctx, r))
+					}
+				}()
+				res = ec._Torrent_files(ctx, field, obj)
+				if res == graphql.Null {
+					atomic.AddUint32(&fs.Invalids, 1)
+				}
+				return res
+			}
+
+			if field.Deferrable != nil {
+				dfs, ok := deferred[field.Deferrable.Label]
+				di := 0
+				if ok {
+					dfs.AddField(field)
+					di = len(dfs.Values) - 1
+				} else {
+					dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
+					deferred[field.Deferrable.Label] = dfs
+				}
+				dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
+					return innerFunc(ctx, dfs)
+				})
+
+				// don't run the out.Concurrently() call below
+				out.Values[i] = graphql.Null
+				continue
+			}
+
+			out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
+		case "excludedFiles":
+			field := field
+
+			innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
+				defer func() {
+					if r := recover(); r != nil {
+						ec.Error(ctx, ec.Recover(ctx, r))
+					}
+				}()
+				res = ec._Torrent_excludedFiles(ctx, field, obj)
+				if res == graphql.Null {
+					atomic.AddUint32(&fs.Invalids, 1)
+				}
+				return res
+			}
+
+			if field.Deferrable != nil {
+				dfs, ok := deferred[field.Deferrable.Label]
+				di := 0
+				if ok {
+					dfs.AddField(field)
+					di = len(dfs.Values) - 1
+				} else {
+					dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
+					deferred[field.Deferrable.Label] = dfs
+				}
+				dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
+					return innerFunc(ctx, dfs)
+				})
+
+				// don't run the out.Concurrently() call below
+				out.Values[i] = graphql.Null
+				continue
+			}
+
+			out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
+		case "peers":
+			field := field
+
+			innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
+				defer func() {
+					if r := recover(); r != nil {
+						ec.Error(ctx, ec.Recover(ctx, r))
+					}
+				}()
+				res = ec._Torrent_peers(ctx, field, obj)
+				if res == graphql.Null {
+					atomic.AddUint32(&fs.Invalids, 1)
+				}
+				return res
+			}
+
+			if field.Deferrable != nil {
+				dfs, ok := deferred[field.Deferrable.Label]
+				di := 0
+				if ok {
+					dfs.AddField(field)
+					di = len(dfs.Values) - 1
+				} else {
+					dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
+					deferred[field.Deferrable.Label] = dfs
+				}
+				dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
+					return innerFunc(ctx, dfs)
+				})
+
+				// don't run the out.Concurrently() call below
+				out.Values[i] = graphql.Null
+				continue
+			}
+
+			out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
+		default:
+			panic("unknown field " + strconv.Quote(field.Name))
+		}
+	}
+	out.Dispatch(ctx)
+	if out.Invalids > 0 {
+		return graphql.Null
+	}
+
+	atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+	for label, dfs := range deferred {
+		ec.processDeferredGroup(graphql.DeferredGroup{
+			Label:    label,
+			Path:     graphql.GetPath(ctx),
+			FieldSet: dfs,
+			Context:  ctx,
+		})
+	}
+
+	return out
+}
+
+var torrentFileImplementors = []string{"TorrentFile"}
+
+func (ec *executionContext) _TorrentFile(ctx context.Context, sel ast.SelectionSet, obj *model.TorrentFile) graphql.Marshaler {
+	fields := graphql.CollectFields(ec.OperationContext, sel, torrentFileImplementors)
+
+	out := graphql.NewFieldSet(fields)
+	deferred := make(map[string]*graphql.FieldSet)
+	for i, field := range fields {
+		switch field.Name {
+		case "__typename":
+			out.Values[i] = graphql.MarshalString("TorrentFile")
+		case "filename":
+			out.Values[i] = ec._TorrentFile_filename(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "size":
+			out.Values[i] = ec._TorrentFile_size(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "bytesCompleted":
+			out.Values[i] = ec._TorrentFile_bytesCompleted(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		default:
+			panic("unknown field " + strconv.Quote(field.Name))
+		}
+	}
+	out.Dispatch(ctx)
+	if out.Invalids > 0 {
+		return graphql.Null
+	}
+
+	atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+	for label, dfs := range deferred {
+		ec.processDeferredGroup(graphql.DeferredGroup{
+			Label:    label,
+			Path:     graphql.GetPath(ctx),
+			FieldSet: dfs,
+			Context:  ctx,
+		})
+	}
+
+	return out
+}
+
+var torrentPeerImplementors = []string{"TorrentPeer"}
+
+func (ec *executionContext) _TorrentPeer(ctx context.Context, sel ast.SelectionSet, obj *model.TorrentPeer) graphql.Marshaler {
+	fields := graphql.CollectFields(ec.OperationContext, sel, torrentPeerImplementors)
+
+	out := graphql.NewFieldSet(fields)
+	deferred := make(map[string]*graphql.FieldSet)
+	for i, field := range fields {
+		switch field.Name {
+		case "__typename":
+			out.Values[i] = graphql.MarshalString("TorrentPeer")
+		case "ip":
+			out.Values[i] = ec._TorrentPeer_ip(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "downloadRate":
+			out.Values[i] = ec._TorrentPeer_downloadRate(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "discovery":
+			out.Values[i] = ec._TorrentPeer_discovery(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "port":
+			out.Values[i] = ec._TorrentPeer_port(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "clientName":
+			out.Values[i] = ec._TorrentPeer_clientName(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		default:
+			panic("unknown field " + strconv.Quote(field.Name))
+		}
+	}
+	out.Dispatch(ctx)
+	if out.Invalids > 0 {
+		return graphql.Null
+	}
+
+	atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+	for label, dfs := range deferred {
+		ec.processDeferredGroup(graphql.DeferredGroup{
+			Label:    label,
+			Path:     graphql.GetPath(ctx),
+			FieldSet: dfs,
+			Context:  ctx,
+		})
+	}
+
+	return out
+}
+
+var __DirectiveImplementors = []string{"__Directive"}
+
+func (ec *executionContext) ___Directive(ctx context.Context, sel ast.SelectionSet, obj *introspection.Directive) graphql.Marshaler {
+	fields := graphql.CollectFields(ec.OperationContext, sel, __DirectiveImplementors)
+
+	out := graphql.NewFieldSet(fields)
+	deferred := make(map[string]*graphql.FieldSet)
+	for i, field := range fields {
+		switch field.Name {
+		case "__typename":
+			out.Values[i] = graphql.MarshalString("__Directive")
+		case "name":
+			out.Values[i] = ec.___Directive_name(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "description":
+			out.Values[i] = ec.___Directive_description(ctx, field, obj)
+		case "locations":
+			out.Values[i] = ec.___Directive_locations(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "args":
+			out.Values[i] = ec.___Directive_args(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "isRepeatable":
+			out.Values[i] = ec.___Directive_isRepeatable(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		default:
+			panic("unknown field " + strconv.Quote(field.Name))
+		}
+	}
+	out.Dispatch(ctx)
+	if out.Invalids > 0 {
+		return graphql.Null
+	}
+
+	atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+	for label, dfs := range deferred {
+		ec.processDeferredGroup(graphql.DeferredGroup{
+			Label:    label,
+			Path:     graphql.GetPath(ctx),
+			FieldSet: dfs,
+			Context:  ctx,
+		})
+	}
+
+	return out
+}
+
+var __EnumValueImplementors = []string{"__EnumValue"}
+
+func (ec *executionContext) ___EnumValue(ctx context.Context, sel ast.SelectionSet, obj *introspection.EnumValue) graphql.Marshaler {
+	fields := graphql.CollectFields(ec.OperationContext, sel, __EnumValueImplementors)
+
+	out := graphql.NewFieldSet(fields)
+	deferred := make(map[string]*graphql.FieldSet)
+	for i, field := range fields {
+		switch field.Name {
+		case "__typename":
+			out.Values[i] = graphql.MarshalString("__EnumValue")
+		case "name":
+			out.Values[i] = ec.___EnumValue_name(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "description":
+			out.Values[i] = ec.___EnumValue_description(ctx, field, obj)
+		case "isDeprecated":
+			out.Values[i] = ec.___EnumValue_isDeprecated(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "deprecationReason":
+			out.Values[i] = ec.___EnumValue_deprecationReason(ctx, field, obj)
+		default:
+			panic("unknown field " + strconv.Quote(field.Name))
+		}
+	}
+	out.Dispatch(ctx)
+	if out.Invalids > 0 {
+		return graphql.Null
+	}
+
+	atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+	for label, dfs := range deferred {
+		ec.processDeferredGroup(graphql.DeferredGroup{
+			Label:    label,
+			Path:     graphql.GetPath(ctx),
+			FieldSet: dfs,
+			Context:  ctx,
+		})
+	}
+
+	return out
+}
+
+var __FieldImplementors = []string{"__Field"}
+
+func (ec *executionContext) ___Field(ctx context.Context, sel ast.SelectionSet, obj *introspection.Field) graphql.Marshaler {
+	fields := graphql.CollectFields(ec.OperationContext, sel, __FieldImplementors)
+
+	out := graphql.NewFieldSet(fields)
+	deferred := make(map[string]*graphql.FieldSet)
+	for i, field := range fields {
+		switch field.Name {
+		case "__typename":
+			out.Values[i] = graphql.MarshalString("__Field")
+		case "name":
+			out.Values[i] = ec.___Field_name(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "description":
+			out.Values[i] = ec.___Field_description(ctx, field, obj)
+		case "args":
+			out.Values[i] = ec.___Field_args(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "type":
+			out.Values[i] = ec.___Field_type(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "isDeprecated":
+			out.Values[i] = ec.___Field_isDeprecated(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "deprecationReason":
+			out.Values[i] = ec.___Field_deprecationReason(ctx, field, obj)
+		default:
+			panic("unknown field " + strconv.Quote(field.Name))
+		}
+	}
+	out.Dispatch(ctx)
+	if out.Invalids > 0 {
+		return graphql.Null
+	}
+
+	atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+	for label, dfs := range deferred {
+		ec.processDeferredGroup(graphql.DeferredGroup{
+			Label:    label,
+			Path:     graphql.GetPath(ctx),
+			FieldSet: dfs,
+			Context:  ctx,
+		})
+	}
+
+	return out
+}
+
+var __InputValueImplementors = []string{"__InputValue"}
+
+func (ec *executionContext) ___InputValue(ctx context.Context, sel ast.SelectionSet, obj *introspection.InputValue) graphql.Marshaler {
+	fields := graphql.CollectFields(ec.OperationContext, sel, __InputValueImplementors)
+
+	out := graphql.NewFieldSet(fields)
+	deferred := make(map[string]*graphql.FieldSet)
+	for i, field := range fields {
+		switch field.Name {
+		case "__typename":
+			out.Values[i] = graphql.MarshalString("__InputValue")
+		case "name":
+			out.Values[i] = ec.___InputValue_name(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "description":
+			out.Values[i] = ec.___InputValue_description(ctx, field, obj)
+		case "type":
+			out.Values[i] = ec.___InputValue_type(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "defaultValue":
+			out.Values[i] = ec.___InputValue_defaultValue(ctx, field, obj)
+		default:
+			panic("unknown field " + strconv.Quote(field.Name))
+		}
+	}
+	out.Dispatch(ctx)
+	if out.Invalids > 0 {
+		return graphql.Null
+	}
+
+	atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+	for label, dfs := range deferred {
+		ec.processDeferredGroup(graphql.DeferredGroup{
+			Label:    label,
+			Path:     graphql.GetPath(ctx),
+			FieldSet: dfs,
+			Context:  ctx,
+		})
+	}
+
+	return out
+}
+
+var __SchemaImplementors = []string{"__Schema"}
+
+func (ec *executionContext) ___Schema(ctx context.Context, sel ast.SelectionSet, obj *introspection.Schema) graphql.Marshaler {
+	fields := graphql.CollectFields(ec.OperationContext, sel, __SchemaImplementors)
+
+	out := graphql.NewFieldSet(fields)
+	deferred := make(map[string]*graphql.FieldSet)
+	for i, field := range fields {
+		switch field.Name {
+		case "__typename":
+			out.Values[i] = graphql.MarshalString("__Schema")
+		case "description":
+			out.Values[i] = ec.___Schema_description(ctx, field, obj)
+		case "types":
+			out.Values[i] = ec.___Schema_types(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "queryType":
+			out.Values[i] = ec.___Schema_queryType(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "mutationType":
+			out.Values[i] = ec.___Schema_mutationType(ctx, field, obj)
+		case "subscriptionType":
+			out.Values[i] = ec.___Schema_subscriptionType(ctx, field, obj)
+		case "directives":
+			out.Values[i] = ec.___Schema_directives(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		default:
+			panic("unknown field " + strconv.Quote(field.Name))
+		}
+	}
+	out.Dispatch(ctx)
+	if out.Invalids > 0 {
+		return graphql.Null
+	}
+
+	atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+	for label, dfs := range deferred {
+		ec.processDeferredGroup(graphql.DeferredGroup{
+			Label:    label,
+			Path:     graphql.GetPath(ctx),
+			FieldSet: dfs,
+			Context:  ctx,
+		})
+	}
+
+	return out
+}
+
+var __TypeImplementors = []string{"__Type"}
+
+func (ec *executionContext) ___Type(ctx context.Context, sel ast.SelectionSet, obj *introspection.Type) graphql.Marshaler {
+	fields := graphql.CollectFields(ec.OperationContext, sel, __TypeImplementors)
+
+	out := graphql.NewFieldSet(fields)
+	deferred := make(map[string]*graphql.FieldSet)
+	for i, field := range fields {
+		switch field.Name {
+		case "__typename":
+			out.Values[i] = graphql.MarshalString("__Type")
+		case "kind":
+			out.Values[i] = ec.___Type_kind(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "name":
+			out.Values[i] = ec.___Type_name(ctx, field, obj)
+		case "description":
+			out.Values[i] = ec.___Type_description(ctx, field, obj)
+		case "fields":
+			out.Values[i] = ec.___Type_fields(ctx, field, obj)
+		case "interfaces":
+			out.Values[i] = ec.___Type_interfaces(ctx, field, obj)
+		case "possibleTypes":
+			out.Values[i] = ec.___Type_possibleTypes(ctx, field, obj)
+		case "enumValues":
+			out.Values[i] = ec.___Type_enumValues(ctx, field, obj)
+		case "inputFields":
+			out.Values[i] = ec.___Type_inputFields(ctx, field, obj)
+		case "ofType":
+			out.Values[i] = ec.___Type_ofType(ctx, field, obj)
+		case "specifiedByURL":
+			out.Values[i] = ec.___Type_specifiedByURL(ctx, field, obj)
+		default:
+			panic("unknown field " + strconv.Quote(field.Name))
+		}
+	}
+	out.Dispatch(ctx)
+	if out.Invalids > 0 {
+		return graphql.Null
+	}
+
+	atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+	for label, dfs := range deferred {
+		ec.processDeferredGroup(graphql.DeferredGroup{
+			Label:    label,
+			Path:     graphql.GetPath(ctx),
+			FieldSet: dfs,
+			Context:  ctx,
+		})
+	}
+
+	return out
+}
+
+// endregion **************************** object.gotpl ****************************
+
+// region    ***************************** type.gotpl *****************************
+
+func (ec *executionContext) unmarshalNBoolean2bool(ctx context.Context, v interface{}) (bool, error) {
+	res, err := graphql.UnmarshalBoolean(v)
+	return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.SelectionSet, v bool) graphql.Marshaler {
+	res := graphql.MarshalBoolean(v)
+	if res == graphql.Null {
+		if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+			ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+		}
+	}
+	return res
+}
+
+func (ec *executionContext) unmarshalNFloat2float64(ctx context.Context, v interface{}) (float64, error) {
+	res, err := graphql.UnmarshalFloatContext(ctx, v)
+	return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalNFloat2float64(ctx context.Context, sel ast.SelectionSet, v float64) graphql.Marshaler {
+	res := graphql.MarshalFloatContext(v)
+	if res == graphql.Null {
+		if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+			ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+		}
+	}
+	return graphql.WrapContextMarshaler(ctx, res)
+}
+
+func (ec *executionContext) unmarshalNInt2int64(ctx context.Context, v interface{}) (int64, error) {
+	res, err := graphql.UnmarshalInt64(v)
+	return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalNInt2int64(ctx context.Context, sel ast.SelectionSet, v int64) graphql.Marshaler {
+	res := graphql.MarshalInt64(v)
+	if res == graphql.Null {
+		if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+			ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+		}
+	}
+	return res
+}
+
+func (ec *executionContext) unmarshalNString2string(ctx context.Context, v interface{}) (string, error) {
+	res, err := graphql.UnmarshalString(v)
+	return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalNString2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler {
+	res := graphql.MarshalString(v)
+	if res == graphql.Null {
+		if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+			ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+		}
+	}
+	return res
+}
+
+func (ec *executionContext) marshalNTorrent2ᚕᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.Torrent) graphql.Marshaler {
+	ret := make(graphql.Array, len(v))
+	var wg sync.WaitGroup
+	isLen1 := len(v) == 1
+	if !isLen1 {
+		wg.Add(len(v))
+	}
+	for i := range v {
+		i := i
+		fc := &graphql.FieldContext{
+			Index:  &i,
+			Result: &v[i],
+		}
+		ctx := graphql.WithFieldContext(ctx, fc)
+		f := func(i int) {
+			defer func() {
+				if r := recover(); r != nil {
+					ec.Error(ctx, ec.Recover(ctx, r))
+					ret = nil
+				}
+			}()
+			if !isLen1 {
+				defer wg.Done()
+			}
+			ret[i] = ec.marshalNTorrent2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrent(ctx, sel, v[i])
+		}
+		if isLen1 {
+			f(i)
+		} else {
+			go f(i)
+		}
+
+	}
+	wg.Wait()
+
+	for _, e := range ret {
+		if e == graphql.Null {
+			return graphql.Null
+		}
+	}
+
+	return ret
+}
+
+func (ec *executionContext) marshalNTorrent2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrent(ctx context.Context, sel ast.SelectionSet, v *model.Torrent) graphql.Marshaler {
+	if v == nil {
+		if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+			ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+		}
+		return graphql.Null
+	}
+	return ec._Torrent(ctx, sel, v)
+}
+
+func (ec *executionContext) marshalNTorrentFile2ᚕᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentFileᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.TorrentFile) graphql.Marshaler {
+	ret := make(graphql.Array, len(v))
+	var wg sync.WaitGroup
+	isLen1 := len(v) == 1
+	if !isLen1 {
+		wg.Add(len(v))
+	}
+	for i := range v {
+		i := i
+		fc := &graphql.FieldContext{
+			Index:  &i,
+			Result: &v[i],
+		}
+		ctx := graphql.WithFieldContext(ctx, fc)
+		f := func(i int) {
+			defer func() {
+				if r := recover(); r != nil {
+					ec.Error(ctx, ec.Recover(ctx, r))
+					ret = nil
+				}
+			}()
+			if !isLen1 {
+				defer wg.Done()
+			}
+			ret[i] = ec.marshalNTorrentFile2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentFile(ctx, sel, v[i])
+		}
+		if isLen1 {
+			f(i)
+		} else {
+			go f(i)
+		}
+
+	}
+	wg.Wait()
+
+	for _, e := range ret {
+		if e == graphql.Null {
+			return graphql.Null
+		}
+	}
+
+	return ret
+}
+
+func (ec *executionContext) marshalNTorrentFile2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentFile(ctx context.Context, sel ast.SelectionSet, v *model.TorrentFile) graphql.Marshaler {
+	if v == nil {
+		if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+			ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+		}
+		return graphql.Null
+	}
+	return ec._TorrentFile(ctx, sel, v)
+}
+
+func (ec *executionContext) unmarshalNTorrentFilter2gitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentFilter(ctx context.Context, v interface{}) (model.TorrentFilter, error) {
+	res, err := ec.unmarshalInputTorrentFilter(ctx, v)
+	return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalNTorrentPeer2ᚕᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentPeerᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.TorrentPeer) graphql.Marshaler {
+	ret := make(graphql.Array, len(v))
+	var wg sync.WaitGroup
+	isLen1 := len(v) == 1
+	if !isLen1 {
+		wg.Add(len(v))
+	}
+	for i := range v {
+		i := i
+		fc := &graphql.FieldContext{
+			Index:  &i,
+			Result: &v[i],
+		}
+		ctx := graphql.WithFieldContext(ctx, fc)
+		f := func(i int) {
+			defer func() {
+				if r := recover(); r != nil {
+					ec.Error(ctx, ec.Recover(ctx, r))
+					ret = nil
+				}
+			}()
+			if !isLen1 {
+				defer wg.Done()
+			}
+			ret[i] = ec.marshalNTorrentPeer2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentPeer(ctx, sel, v[i])
+		}
+		if isLen1 {
+			f(i)
+		} else {
+			go f(i)
+		}
+
+	}
+	wg.Wait()
+
+	for _, e := range ret {
+		if e == graphql.Null {
+			return graphql.Null
+		}
+	}
+
+	return ret
+}
+
+func (ec *executionContext) marshalNTorrentPeer2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentPeer(ctx context.Context, sel ast.SelectionSet, v *model.TorrentPeer) graphql.Marshaler {
+	if v == nil {
+		if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+			ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+		}
+		return graphql.Null
+	}
+	return ec._TorrentPeer(ctx, sel, v)
+}
+
+func (ec *executionContext) marshalN__Directive2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirective(ctx context.Context, sel ast.SelectionSet, v introspection.Directive) graphql.Marshaler {
+	return ec.___Directive(ctx, sel, &v)
+}
+
+func (ec *executionContext) marshalN__Directive2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirectiveᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.Directive) graphql.Marshaler {
+	ret := make(graphql.Array, len(v))
+	var wg sync.WaitGroup
+	isLen1 := len(v) == 1
+	if !isLen1 {
+		wg.Add(len(v))
+	}
+	for i := range v {
+		i := i
+		fc := &graphql.FieldContext{
+			Index:  &i,
+			Result: &v[i],
+		}
+		ctx := graphql.WithFieldContext(ctx, fc)
+		f := func(i int) {
+			defer func() {
+				if r := recover(); r != nil {
+					ec.Error(ctx, ec.Recover(ctx, r))
+					ret = nil
+				}
+			}()
+			if !isLen1 {
+				defer wg.Done()
+			}
+			ret[i] = ec.marshalN__Directive2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirective(ctx, sel, v[i])
+		}
+		if isLen1 {
+			f(i)
+		} else {
+			go f(i)
+		}
+
+	}
+	wg.Wait()
+
+	for _, e := range ret {
+		if e == graphql.Null {
+			return graphql.Null
+		}
+	}
+
+	return ret
+}
+
+func (ec *executionContext) unmarshalN__DirectiveLocation2string(ctx context.Context, v interface{}) (string, error) {
+	res, err := graphql.UnmarshalString(v)
+	return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalN__DirectiveLocation2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler {
+	res := graphql.MarshalString(v)
+	if res == graphql.Null {
+		if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+			ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+		}
+	}
+	return res
+}
+
+func (ec *executionContext) unmarshalN__DirectiveLocation2ᚕstringᚄ(ctx context.Context, v interface{}) ([]string, error) {
+	var vSlice []interface{}
+	if v != nil {
+		vSlice = graphql.CoerceList(v)
+	}
+	var err error
+	res := make([]string, len(vSlice))
+	for i := range vSlice {
+		ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i))
+		res[i], err = ec.unmarshalN__DirectiveLocation2string(ctx, vSlice[i])
+		if err != nil {
+			return nil, err
+		}
+	}
+	return res, nil
+}
+
+func (ec *executionContext) marshalN__DirectiveLocation2ᚕstringᚄ(ctx context.Context, sel ast.SelectionSet, v []string) graphql.Marshaler {
+	ret := make(graphql.Array, len(v))
+	var wg sync.WaitGroup
+	isLen1 := len(v) == 1
+	if !isLen1 {
+		wg.Add(len(v))
+	}
+	for i := range v {
+		i := i
+		fc := &graphql.FieldContext{
+			Index:  &i,
+			Result: &v[i],
+		}
+		ctx := graphql.WithFieldContext(ctx, fc)
+		f := func(i int) {
+			defer func() {
+				if r := recover(); r != nil {
+					ec.Error(ctx, ec.Recover(ctx, r))
+					ret = nil
+				}
+			}()
+			if !isLen1 {
+				defer wg.Done()
+			}
+			ret[i] = ec.marshalN__DirectiveLocation2string(ctx, sel, v[i])
+		}
+		if isLen1 {
+			f(i)
+		} else {
+			go f(i)
+		}
+
+	}
+	wg.Wait()
+
+	for _, e := range ret {
+		if e == graphql.Null {
+			return graphql.Null
+		}
+	}
+
+	return ret
+}
+
+func (ec *executionContext) marshalN__EnumValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValue(ctx context.Context, sel ast.SelectionSet, v introspection.EnumValue) graphql.Marshaler {
+	return ec.___EnumValue(ctx, sel, &v)
+}
+
+func (ec *executionContext) marshalN__Field2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐField(ctx context.Context, sel ast.SelectionSet, v introspection.Field) graphql.Marshaler {
+	return ec.___Field(ctx, sel, &v)
+}
+
+func (ec *executionContext) marshalN__InputValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValue(ctx context.Context, sel ast.SelectionSet, v introspection.InputValue) graphql.Marshaler {
+	return ec.___InputValue(ctx, sel, &v)
+}
+
+func (ec *executionContext) marshalN__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.InputValue) graphql.Marshaler {
+	ret := make(graphql.Array, len(v))
+	var wg sync.WaitGroup
+	isLen1 := len(v) == 1
+	if !isLen1 {
+		wg.Add(len(v))
+	}
+	for i := range v {
+		i := i
+		fc := &graphql.FieldContext{
+			Index:  &i,
+			Result: &v[i],
+		}
+		ctx := graphql.WithFieldContext(ctx, fc)
+		f := func(i int) {
+			defer func() {
+				if r := recover(); r != nil {
+					ec.Error(ctx, ec.Recover(ctx, r))
+					ret = nil
+				}
+			}()
+			if !isLen1 {
+				defer wg.Done()
+			}
+			ret[i] = ec.marshalN__InputValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValue(ctx, sel, v[i])
+		}
+		if isLen1 {
+			f(i)
+		} else {
+			go f(i)
+		}
+
+	}
+	wg.Wait()
+
+	for _, e := range ret {
+		if e == graphql.Null {
+			return graphql.Null
+		}
+	}
+
+	return ret
+}
+
+func (ec *executionContext) marshalN__Type2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx context.Context, sel ast.SelectionSet, v introspection.Type) graphql.Marshaler {
+	return ec.___Type(ctx, sel, &v)
+}
+
+func (ec *executionContext) marshalN__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐTypeᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.Type) graphql.Marshaler {
+	ret := make(graphql.Array, len(v))
+	var wg sync.WaitGroup
+	isLen1 := len(v) == 1
+	if !isLen1 {
+		wg.Add(len(v))
+	}
+	for i := range v {
+		i := i
+		fc := &graphql.FieldContext{
+			Index:  &i,
+			Result: &v[i],
+		}
+		ctx := graphql.WithFieldContext(ctx, fc)
+		f := func(i int) {
+			defer func() {
+				if r := recover(); r != nil {
+					ec.Error(ctx, ec.Recover(ctx, r))
+					ret = nil
+				}
+			}()
+			if !isLen1 {
+				defer wg.Done()
+			}
+			ret[i] = ec.marshalN__Type2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, sel, v[i])
+		}
+		if isLen1 {
+			f(i)
+		} else {
+			go f(i)
+		}
+
+	}
+	wg.Wait()
+
+	for _, e := range ret {
+		if e == graphql.Null {
+			return graphql.Null
+		}
+	}
+
+	return ret
+}
+
+func (ec *executionContext) marshalN__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx context.Context, sel ast.SelectionSet, v *introspection.Type) graphql.Marshaler {
+	if v == nil {
+		if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+			ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+		}
+		return graphql.Null
+	}
+	return ec.___Type(ctx, sel, v)
+}
+
+func (ec *executionContext) unmarshalN__TypeKind2string(ctx context.Context, v interface{}) (string, error) {
+	res, err := graphql.UnmarshalString(v)
+	return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalN__TypeKind2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler {
+	res := graphql.MarshalString(v)
+	if res == graphql.Null {
+		if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+			ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+		}
+	}
+	return res
+}
+
+func (ec *executionContext) unmarshalOBoolean2bool(ctx context.Context, v interface{}) (bool, error) {
+	res, err := graphql.UnmarshalBoolean(v)
+	return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalOBoolean2bool(ctx context.Context, sel ast.SelectionSet, v bool) graphql.Marshaler {
+	res := graphql.MarshalBoolean(v)
+	return res
+}
+
+func (ec *executionContext) unmarshalOBoolean2ᚖbool(ctx context.Context, v interface{}) (*bool, error) {
+	if v == nil {
+		return nil, nil
+	}
+	res, err := graphql.UnmarshalBoolean(v)
+	return &res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast.SelectionSet, v *bool) graphql.Marshaler {
+	if v == nil {
+		return graphql.Null
+	}
+	res := graphql.MarshalBoolean(*v)
+	return res
+}
+
+func (ec *executionContext) unmarshalODateTime2ᚖtimeᚐTime(ctx context.Context, v interface{}) (*time.Time, error) {
+	if v == nil {
+		return nil, nil
+	}
+	res, err := graphql.UnmarshalTime(v)
+	return &res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalODateTime2ᚖtimeᚐTime(ctx context.Context, sel ast.SelectionSet, v *time.Time) graphql.Marshaler {
+	if v == nil {
+		return graphql.Null
+	}
+	res := graphql.MarshalTime(*v)
+	return res
+}
+
+func (ec *executionContext) unmarshalOInt2ᚕint64ᚄ(ctx context.Context, v interface{}) ([]int64, error) {
+	if v == nil {
+		return nil, nil
+	}
+	var vSlice []interface{}
+	if v != nil {
+		vSlice = graphql.CoerceList(v)
+	}
+	var err error
+	res := make([]int64, len(vSlice))
+	for i := range vSlice {
+		ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i))
+		res[i], err = ec.unmarshalNInt2int64(ctx, vSlice[i])
+		if err != nil {
+			return nil, err
+		}
+	}
+	return res, nil
+}
+
+func (ec *executionContext) marshalOInt2ᚕint64ᚄ(ctx context.Context, sel ast.SelectionSet, v []int64) graphql.Marshaler {
+	if v == nil {
+		return graphql.Null
+	}
+	ret := make(graphql.Array, len(v))
+	for i := range v {
+		ret[i] = ec.marshalNInt2int64(ctx, sel, v[i])
+	}
+
+	for _, e := range ret {
+		if e == graphql.Null {
+			return graphql.Null
+		}
+	}
+
+	return ret
+}
+
+func (ec *executionContext) unmarshalOInt2ᚖint64(ctx context.Context, v interface{}) (*int64, error) {
+	if v == nil {
+		return nil, nil
+	}
+	res, err := graphql.UnmarshalInt64(v)
+	return &res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalOInt2ᚖint64(ctx context.Context, sel ast.SelectionSet, v *int64) graphql.Marshaler {
+	if v == nil {
+		return graphql.Null
+	}
+	res := graphql.MarshalInt64(*v)
+	return res
+}
+
+func (ec *executionContext) unmarshalOIntFilter2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐIntFilter(ctx context.Context, v interface{}) (*model.IntFilter, error) {
+	if v == nil {
+		return nil, nil
+	}
+	res, err := ec.unmarshalInputIntFilter(ctx, v)
+	return &res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalOMutation2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐMutation(ctx context.Context, sel ast.SelectionSet, v *model.Mutation) graphql.Marshaler {
+	if v == nil {
+		return graphql.Null
+	}
+	return ec._Mutation(ctx, sel)
+}
+
+func (ec *executionContext) unmarshalOPagination2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐPagination(ctx context.Context, v interface{}) (*model.Pagination, error) {
+	if v == nil {
+		return nil, nil
+	}
+	res, err := ec.unmarshalInputPagination(ctx, v)
+	return &res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalOQuery2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐQuery(ctx context.Context, sel ast.SelectionSet, v *model.Query) graphql.Marshaler {
+	if v == nil {
+		return graphql.Null
+	}
+	return ec._Query(ctx, sel)
+}
+
+func (ec *executionContext) unmarshalOString2ᚕstringᚄ(ctx context.Context, v interface{}) ([]string, error) {
+	if v == nil {
+		return nil, nil
+	}
+	var vSlice []interface{}
+	if v != nil {
+		vSlice = graphql.CoerceList(v)
+	}
+	var err error
+	res := make([]string, len(vSlice))
+	for i := range vSlice {
+		ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i))
+		res[i], err = ec.unmarshalNString2string(ctx, vSlice[i])
+		if err != nil {
+			return nil, err
+		}
+	}
+	return res, nil
+}
+
+func (ec *executionContext) marshalOString2ᚕstringᚄ(ctx context.Context, sel ast.SelectionSet, v []string) graphql.Marshaler {
+	if v == nil {
+		return graphql.Null
+	}
+	ret := make(graphql.Array, len(v))
+	for i := range v {
+		ret[i] = ec.marshalNString2string(ctx, sel, v[i])
+	}
+
+	for _, e := range ret {
+		if e == graphql.Null {
+			return graphql.Null
+		}
+	}
+
+	return ret
+}
+
+func (ec *executionContext) unmarshalOString2ᚖstring(ctx context.Context, v interface{}) (*string, error) {
+	if v == nil {
+		return nil, nil
+	}
+	res, err := graphql.UnmarshalString(v)
+	return &res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalOString2ᚖstring(ctx context.Context, sel ast.SelectionSet, v *string) graphql.Marshaler {
+	if v == nil {
+		return graphql.Null
+	}
+	res := graphql.MarshalString(*v)
+	return res
+}
+
+func (ec *executionContext) unmarshalOStringFilter2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐStringFilter(ctx context.Context, v interface{}) (*model.StringFilter, error) {
+	if v == nil {
+		return nil, nil
+	}
+	res, err := ec.unmarshalInputStringFilter(ctx, v)
+	return &res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) unmarshalOTorrentsFilter2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentsFilter(ctx context.Context, v interface{}) (*model.TorrentsFilter, error) {
+	if v == nil {
+		return nil, nil
+	}
+	res, err := ec.unmarshalInputTorrentsFilter(ctx, v)
+	return &res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalO__EnumValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValueᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.EnumValue) graphql.Marshaler {
+	if v == nil {
+		return graphql.Null
+	}
+	ret := make(graphql.Array, len(v))
+	var wg sync.WaitGroup
+	isLen1 := len(v) == 1
+	if !isLen1 {
+		wg.Add(len(v))
+	}
+	for i := range v {
+		i := i
+		fc := &graphql.FieldContext{
+			Index:  &i,
+			Result: &v[i],
+		}
+		ctx := graphql.WithFieldContext(ctx, fc)
+		f := func(i int) {
+			defer func() {
+				if r := recover(); r != nil {
+					ec.Error(ctx, ec.Recover(ctx, r))
+					ret = nil
+				}
+			}()
+			if !isLen1 {
+				defer wg.Done()
+			}
+			ret[i] = ec.marshalN__EnumValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValue(ctx, sel, v[i])
+		}
+		if isLen1 {
+			f(i)
+		} else {
+			go f(i)
+		}
+
+	}
+	wg.Wait()
+
+	for _, e := range ret {
+		if e == graphql.Null {
+			return graphql.Null
+		}
+	}
+
+	return ret
+}
+
+func (ec *executionContext) marshalO__Field2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐFieldᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.Field) graphql.Marshaler {
+	if v == nil {
+		return graphql.Null
+	}
+	ret := make(graphql.Array, len(v))
+	var wg sync.WaitGroup
+	isLen1 := len(v) == 1
+	if !isLen1 {
+		wg.Add(len(v))
+	}
+	for i := range v {
+		i := i
+		fc := &graphql.FieldContext{
+			Index:  &i,
+			Result: &v[i],
+		}
+		ctx := graphql.WithFieldContext(ctx, fc)
+		f := func(i int) {
+			defer func() {
+				if r := recover(); r != nil {
+					ec.Error(ctx, ec.Recover(ctx, r))
+					ret = nil
+				}
+			}()
+			if !isLen1 {
+				defer wg.Done()
+			}
+			ret[i] = ec.marshalN__Field2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐField(ctx, sel, v[i])
+		}
+		if isLen1 {
+			f(i)
+		} else {
+			go f(i)
+		}
+
+	}
+	wg.Wait()
+
+	for _, e := range ret {
+		if e == graphql.Null {
+			return graphql.Null
+		}
+	}
+
+	return ret
+}
+
+func (ec *executionContext) marshalO__InputValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValueᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.InputValue) graphql.Marshaler {
+	if v == nil {
+		return graphql.Null
+	}
+	ret := make(graphql.Array, len(v))
+	var wg sync.WaitGroup
+	isLen1 := len(v) == 1
+	if !isLen1 {
+		wg.Add(len(v))
+	}
+	for i := range v {
+		i := i
+		fc := &graphql.FieldContext{
+			Index:  &i,
+			Result: &v[i],
+		}
+		ctx := graphql.WithFieldContext(ctx, fc)
+		f := func(i int) {
+			defer func() {
+				if r := recover(); r != nil {
+					ec.Error(ctx, ec.Recover(ctx, r))
+					ret = nil
+				}
+			}()
+			if !isLen1 {
+				defer wg.Done()
+			}
+			ret[i] = ec.marshalN__InputValue2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐInputValue(ctx, sel, v[i])
+		}
+		if isLen1 {
+			f(i)
+		} else {
+			go f(i)
+		}
+
+	}
+	wg.Wait()
+
+	for _, e := range ret {
+		if e == graphql.Null {
+			return graphql.Null
+		}
+	}
+
+	return ret
+}
+
+func (ec *executionContext) marshalO__Schema2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐSchema(ctx context.Context, sel ast.SelectionSet, v *introspection.Schema) graphql.Marshaler {
+	if v == nil {
+		return graphql.Null
+	}
+	return ec.___Schema(ctx, sel, v)
+}
+
+func (ec *executionContext) marshalO__Type2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐTypeᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.Type) graphql.Marshaler {
+	if v == nil {
+		return graphql.Null
+	}
+	ret := make(graphql.Array, len(v))
+	var wg sync.WaitGroup
+	isLen1 := len(v) == 1
+	if !isLen1 {
+		wg.Add(len(v))
+	}
+	for i := range v {
+		i := i
+		fc := &graphql.FieldContext{
+			Index:  &i,
+			Result: &v[i],
+		}
+		ctx := graphql.WithFieldContext(ctx, fc)
+		f := func(i int) {
+			defer func() {
+				if r := recover(); r != nil {
+					ec.Error(ctx, ec.Recover(ctx, r))
+					ret = nil
+				}
+			}()
+			if !isLen1 {
+				defer wg.Done()
+			}
+			ret[i] = ec.marshalN__Type2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, sel, v[i])
+		}
+		if isLen1 {
+			f(i)
+		} else {
+			go f(i)
+		}
+
+	}
+	wg.Wait()
+
+	for _, e := range ret {
+		if e == graphql.Null {
+			return graphql.Null
+		}
+	}
+
+	return ret
+}
+
+func (ec *executionContext) marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx context.Context, sel ast.SelectionSet, v *introspection.Type) graphql.Marshaler {
+	if v == nil {
+		return graphql.Null
+	}
+	return ec.___Type(ctx, sel, v)
+}
+
+// endregion ***************************** type.gotpl *****************************
diff --git a/src/delivery/graphql/model/filter.go b/src/delivery/graphql/model/filter.go
new file mode 100644
index 0000000..911216b
--- /dev/null
+++ b/src/delivery/graphql/model/filter.go
@@ -0,0 +1,21 @@
+package model
+
+import "slices"
+
+func (f *IntFilter) IsValid(v int64) bool {
+	if f.Eq != nil {
+		return v == *f.Eq
+	} else if f.Gt != nil {
+		return v > *f.Gt
+	} else if f.Gte != nil {
+		return v >= *f.Gte
+	} else if f.Lt != nil {
+		return v < *f.Lt
+	} else if f.Lte != nil {
+		return v <= *f.Lte
+	} else if f.In != nil {
+		return slices.Contains(f.In, v)
+	}
+
+	return true
+}
diff --git a/src/delivery/graphql/model/mappers.go b/src/delivery/graphql/model/mappers.go
new file mode 100644
index 0000000..1523130
--- /dev/null
+++ b/src/delivery/graphql/model/mappers.go
@@ -0,0 +1,24 @@
+package model
+
+import "github.com/anacrolix/torrent"
+
+func MapPeerSource(source torrent.PeerSource) string {
+	switch source {
+	case torrent.PeerSourceDirect:
+		return "Direct"
+	case torrent.PeerSourceUtHolepunch:
+		return "Ut Holepunch"
+	case torrent.PeerSourceDhtAnnouncePeer:
+		return "DHT Announce"
+	case torrent.PeerSourceDhtGetPeers:
+		return "DHT"
+	case torrent.PeerSourceIncoming:
+		return "Incoming"
+	case torrent.PeerSourceTracker:
+		return "Tracker"
+	case torrent.PeerSourcePex:
+		return "PEX"
+	default:
+		return "Unknown"
+	}
+}
diff --git a/src/delivery/graphql/model/models_gen.go b/src/delivery/graphql/model/models_gen.go
new file mode 100644
index 0000000..fec0890
--- /dev/null
+++ b/src/delivery/graphql/model/models_gen.go
@@ -0,0 +1,93 @@
+// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
+
+package model
+
+import (
+	"time"
+
+	"git.kmsign.ru/royalcat/tstor/src/host/controller"
+	"github.com/anacrolix/torrent"
+)
+
+type BooleanFilter struct {
+	Eq *bool `json:"eq,omitempty"`
+}
+
+type DateTimeFilter struct {
+	Eq  *time.Time `json:"eq,omitempty"`
+	Gt  *time.Time `json:"gt,omitempty"`
+	Lt  *time.Time `json:"lt,omitempty"`
+	Gte *time.Time `json:"gte,omitempty"`
+	Lte *time.Time `json:"lte,omitempty"`
+}
+
+type IntFilter struct {
+	Eq  *int64  `json:"eq,omitempty"`
+	Gt  *int64  `json:"gt,omitempty"`
+	Lt  *int64  `json:"lt,omitempty"`
+	Gte *int64  `json:"gte,omitempty"`
+	Lte *int64  `json:"lte,omitempty"`
+	In  []int64 `json:"in,omitempty"`
+}
+
+type Mutation struct {
+}
+
+type Pagination struct {
+	Offset int64 `json:"offset"`
+	Limit  int64 `json:"limit"`
+}
+
+type Query struct {
+}
+
+type Schema struct {
+	Query    *Query    `json:"query,omitempty"`
+	Mutation *Mutation `json:"mutation,omitempty"`
+}
+
+type StringFilter struct {
+	Eq     *string  `json:"eq,omitempty"`
+	Substr *string  `json:"substr,omitempty"`
+	In     []string `json:"in,omitempty"`
+}
+
+type Torrent struct {
+	Name            string              `json:"name"`
+	Infohash        string              `json:"infohash"`
+	BytesCompleted  int64               `json:"bytesCompleted"`
+	TorrentFilePath string              `json:"torrentFilePath"`
+	BytesMissing    int64               `json:"bytesMissing"`
+	Files           []*TorrentFile      `json:"files"`
+	ExcludedFiles   []*TorrentFile      `json:"excludedFiles"`
+	Peers           []*TorrentPeer      `json:"peers"`
+	T               *controller.Torrent `json:"-"`
+}
+
+type TorrentFile struct {
+	Filename       string        `json:"filename"`
+	Size           int64         `json:"size"`
+	BytesCompleted int64         `json:"bytesCompleted"`
+	F              *torrent.File `json:"-"`
+}
+
+type TorrentFilter struct {
+	Everything *bool   `json:"everything,omitempty"`
+	Infohash   *string `json:"infohash,omitempty"`
+}
+
+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:"-"`
+}
+
+type TorrentsFilter struct {
+	Name           *StringFilter `json:"name,omitempty"`
+	BytesCompleted *IntFilter    `json:"bytesCompleted,omitempty"`
+	BytesMissing   *IntFilter    `json:"bytesMissing,omitempty"`
+	PeersCount     *IntFilter    `json:"peersCount,omitempty"`
+}
diff --git a/src/delivery/graphql/oneof.go b/src/delivery/graphql/oneof.go
new file mode 100644
index 0000000..93bf335
--- /dev/null
+++ b/src/delivery/graphql/oneof.go
@@ -0,0 +1,28 @@
+package graph
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/99designs/gqlgen/graphql"
+)
+
+func OneOf(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) {
+	wasValue := false
+	m, ok := obj.(map[string]any)
+	if !ok {
+		return nil, fmt.Errorf("OneOf error, unknow object type: %T", obj)
+	}
+
+	for k, v := range m {
+		if v != nil {
+			if !wasValue {
+				wasValue = true
+			} else {
+				return nil, fmt.Errorf("OneOf with multiple fields: %s", k)
+			}
+		}
+	}
+
+	return next(ctx)
+}
diff --git a/src/delivery/graphql/resolver/mutation.resolvers.go b/src/delivery/graphql/resolver/mutation.resolvers.go
new file mode 100644
index 0000000..e6f16c6
--- /dev/null
+++ b/src/delivery/graphql/resolver/mutation.resolvers.go
@@ -0,0 +1,64 @@
+package resolver
+
+// This file will be automatically regenerated based on the schema, any resolver implementations
+// will be copied through when generating and any unknown code will be moved to the end.
+// Code generated by github.com/99designs/gqlgen version v0.17.43
+
+import (
+	"context"
+
+	graph "git.kmsign.ru/royalcat/tstor/src/delivery/graphql"
+	"git.kmsign.ru/royalcat/tstor/src/delivery/graphql/model"
+)
+
+// ValidateTorrents is the resolver for the validateTorrents field.
+func (r *mutationResolver) ValidateTorrents(ctx context.Context, filter model.TorrentFilter) (bool, error) {
+	if filter.Infohash != nil {
+		t, err := r.Resolver.Service.GetTorrent(*filter.Infohash)
+		if err != nil {
+			return false, err
+		}
+		if t == nil {
+			return false, nil
+		}
+
+		t.ValidateTorrent()
+		return true, nil
+	}
+
+	if filter.Everything != nil && *filter.Everything {
+		torrents, err := r.Resolver.Service.ListTorrents(ctx)
+		if err != nil {
+			return false, err
+		}
+		for _, v := range torrents {
+			if err := v.ValidateTorrent(); err != nil {
+				return false, err
+			}
+		}
+		return true, nil
+	}
+
+	return false, nil
+}
+
+// CleanupTorrents is the resolver for the cleanupTorrents field.
+func (r *mutationResolver) CleanupTorrents(ctx context.Context, files *bool, dryRun bool) (int64, error) {
+	torrents, err := r.Service.ListTorrents(ctx)
+	if err != nil {
+		return 0, err
+	}
+
+	if files != nil && *files {
+		r, err := r.Service.Storage.CleanupFiles(ctx, torrents, dryRun)
+		return int64(r), err
+	} else {
+		r, err := r.Service.Storage.CleanupDirs(ctx, torrents, dryRun)
+		return int64(r), err
+	}
+}
+
+// Mutation returns graph.MutationResolver implementation.
+func (r *Resolver) Mutation() graph.MutationResolver { return &mutationResolver{r} }
+
+type mutationResolver struct{ *Resolver }
diff --git a/src/delivery/graphql/resolver/query.resolvers.go b/src/delivery/graphql/resolver/query.resolvers.go
new file mode 100644
index 0000000..f014380
--- /dev/null
+++ b/src/delivery/graphql/resolver/query.resolvers.go
@@ -0,0 +1,75 @@
+package resolver
+
+// This file will be automatically regenerated based on the schema, any resolver implementations
+// will be copied through when generating and any unknown code will be moved to the end.
+// Code generated by github.com/99designs/gqlgen version v0.17.43
+
+import (
+	"context"
+
+	graph "git.kmsign.ru/royalcat/tstor/src/delivery/graphql"
+	"git.kmsign.ru/royalcat/tstor/src/delivery/graphql/model"
+)
+
+// Torrents is the resolver for the torrents field.
+func (r *queryResolver) Torrents(ctx context.Context, filter *model.TorrentsFilter, pagination *model.Pagination) ([]*model.Torrent, error) {
+	torrents, err := r.Service.ListTorrents(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	filterFuncs := []func(torrent *model.Torrent) bool{}
+
+	if filter != nil {
+		if filter.BytesCompleted != nil {
+			filterFuncs = append(filterFuncs, func(torrent *model.Torrent) bool {
+				return filter.BytesCompleted.IsValid(torrent.BytesCompleted)
+			})
+		}
+		if filter.BytesMissing != nil {
+			filterFuncs = append(filterFuncs, func(torrent *model.Torrent) bool {
+				return filter.BytesMissing.IsValid(torrent.BytesMissing)
+			})
+		}
+		if filter.PeersCount != nil {
+			filterFuncs = append(filterFuncs, func(torrent *model.Torrent) bool {
+				return filter.PeersCount.IsValid(
+					int64(len(torrent.T.Torrent().PeerConns())),
+				)
+			})
+		}
+
+	}
+
+	filterFunc := func(torrent *model.Torrent) bool {
+		for _, f := range filterFuncs {
+			if !f(torrent) {
+				return false
+			}
+		}
+		return true
+	}
+
+	tr := []*model.Torrent{}
+	for _, t := range torrents {
+		d := &model.Torrent{
+			Infohash:       t.InfoHash(),
+			Name:           t.Name(),
+			BytesCompleted: t.BytesCompleted(),
+			BytesMissing:   t.BytesMissing(),
+			T:              t,
+		}
+
+		if !filterFunc(d) {
+			continue
+		}
+		tr = append(tr, d)
+	}
+
+	return tr, nil
+}
+
+// Query returns graph.QueryResolver implementation.
+func (r *Resolver) Query() graph.QueryResolver { return &queryResolver{r} }
+
+type queryResolver struct{ *Resolver }
diff --git a/src/delivery/graphql/resolver/resolver.go b/src/delivery/graphql/resolver/resolver.go
new file mode 100644
index 0000000..1e937e1
--- /dev/null
+++ b/src/delivery/graphql/resolver/resolver.go
@@ -0,0 +1,11 @@
+package resolver
+
+import "git.kmsign.ru/royalcat/tstor/src/host/service"
+
+// This file will not be regenerated automatically.
+//
+// It serves as dependency injection for your app, add any dependencies you require here.
+
+type Resolver struct {
+	Service *service.Service
+}
diff --git a/src/delivery/graphql/resolver/torrent.resolvers.go b/src/delivery/graphql/resolver/torrent.resolvers.go
new file mode 100644
index 0000000..dfc9fd0
--- /dev/null
+++ b/src/delivery/graphql/resolver/torrent.resolvers.go
@@ -0,0 +1,73 @@
+package resolver
+
+// This file will be automatically regenerated based on the schema, any resolver implementations
+// will be copied through when generating and any unknown code will be moved to the end.
+// Code generated by github.com/99designs/gqlgen version v0.17.43
+
+import (
+	"context"
+
+	graph "git.kmsign.ru/royalcat/tstor/src/delivery/graphql"
+	"git.kmsign.ru/royalcat/tstor/src/delivery/graphql/model"
+)
+
+// Name is the resolver for the name field.
+func (r *torrentResolver) Name(ctx context.Context, obj *model.Torrent) (string, error) {
+	return obj.T.Name(), nil
+}
+
+// Files is the resolver for the files field.
+func (r *torrentResolver) Files(ctx context.Context, obj *model.Torrent) ([]*model.TorrentFile, error) {
+	out := []*model.TorrentFile{}
+	files, err := obj.T.Files()
+	if err != nil {
+		return nil, err
+	}
+	for _, f := range files {
+		out = append(out, &model.TorrentFile{
+			Filename:       f.DisplayPath(),
+			Size:           f.Length(),
+			BytesCompleted: f.BytesCompleted(),
+			F:              f,
+		})
+	}
+	return out, nil
+}
+
+// ExcludedFiles is the resolver for the excludedFiles field.
+func (r *torrentResolver) ExcludedFiles(ctx context.Context, obj *model.Torrent) ([]*model.TorrentFile, error) {
+	out := []*model.TorrentFile{}
+	files, err := obj.T.ExcludedFiles()
+	if err != nil {
+		return nil, err
+	}
+	for _, f := range files {
+		out = append(out, &model.TorrentFile{
+			Filename: f.DisplayPath(),
+			Size:     f.Length(),
+			F:        f,
+		})
+	}
+	return out, nil
+}
+
+// Peers is the resolver for the peers field.
+func (r *torrentResolver) Peers(ctx context.Context, obj *model.Torrent) ([]*model.TorrentPeer, error) {
+	peers := []*model.TorrentPeer{}
+	for _, peer := range obj.T.Torrent().PeerConns() {
+		peers = append(peers, &model.TorrentPeer{
+			IP:           peer.RemoteAddr.String(),
+			DownloadRate: peer.DownloadRate(),
+			Discovery:    model.MapPeerSource(peer.Discovery),
+			Port:         int64(peer.PeerListenPort),
+			ClientName:   peer.PeerClientName.Load().(string),
+			F:            peer,
+		})
+	}
+	return peers, nil
+}
+
+// Torrent returns graph.TorrentResolver implementation.
+func (r *Resolver) Torrent() graph.TorrentResolver { return &torrentResolver{r} }
+
+type torrentResolver struct{ *Resolver }
diff --git a/src/delivery/router.go b/src/delivery/router.go
new file mode 100644
index 0000000..4c5f0c4
--- /dev/null
+++ b/src/delivery/router.go
@@ -0,0 +1,35 @@
+package delivery
+
+import (
+	"net/http"
+
+	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"
+	"github.com/99designs/gqlgen/graphql/handler"
+	"github.com/99designs/gqlgen/graphql/handler/extension"
+	"github.com/99designs/gqlgen/graphql/handler/lru"
+	"github.com/99designs/gqlgen/graphql/handler/transport"
+)
+
+func GraphQLHandler(service *service.Service) http.Handler {
+	graphqlHandler := handler.NewDefaultServer(
+		graph.NewExecutableSchema(
+			graph.Config{
+				Resolvers: &resolver.Resolver{Service: service},
+				Directives: graph.DirectiveRoot{
+					OneOf: graph.OneOf,
+				},
+			},
+		),
+	)
+	graphqlHandler.AddTransport(&transport.POST{})
+	graphqlHandler.AddTransport(&transport.Websocket{})
+	graphqlHandler.AddTransport(&transport.SSE{})
+	graphqlHandler.AddTransport(&transport.UrlEncodedForm{})
+	graphqlHandler.SetQueryCache(lru.New(1000))
+	graphqlHandler.Use(extension.Introspection{})
+	graphqlHandler.Use(extension.AutomaticPersistedQuery{Cache: lru.New(100)})
+
+	return graphqlHandler
+}
diff --git a/src/export/fuse/handler.go b/src/export/fuse/handler.go
index 867e466..83c635f 100644
--- a/src/export/fuse/handler.go
+++ b/src/export/fuse/handler.go
@@ -3,14 +3,13 @@
 package fuse
 
 import (
+	"log/slog"
 	"os"
 	"path/filepath"
 	"runtime"
 
 	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
 	"github.com/billziss-gh/cgofuse/fuse"
-
-	"github.com/rs/zerolog/log"
 )
 
 type Handler struct {
@@ -18,16 +17,20 @@ type Handler struct {
 	path           string
 
 	host *fuse.FileSystemHost
+	log  *slog.Logger
 }
 
 func NewHandler(fuseAllowOther bool, path string) *Handler {
 	return &Handler{
 		fuseAllowOther: fuseAllowOther,
 		path:           path,
+		log:            slog.With("component", "fuse-handler").With("path", path),
 	}
 }
 
 func (s *Handler) Mount(vfs vfs.Filesystem) error {
+	log := s.log.With("function", "Mount")
+
 	folder := s.path
 	// On windows, the folder must don't exist
 	if runtime.GOOS == "windows" {
@@ -52,18 +55,20 @@ func (s *Handler) Mount(vfs vfs.Filesystem) error {
 
 		ok := host.Mount(s.path, config)
 		if !ok {
-			log.Error().Str("path", s.path).Msg("error trying to mount filesystem")
+			log.Error("error trying to mount filesystem")
 		}
 	}()
 
 	s.host = host
 
-	log.Info().Str("path", s.path).Msg("starting FUSE mount")
+	log.Info("starting FUSE mount", "path", s.path)
 
 	return nil
 }
 
 func (s *Handler) Unmount() {
+	log := s.log.With("function", "Unmount")
+
 	if s.host == nil {
 		return
 	}
@@ -71,6 +76,6 @@ func (s *Handler) Unmount() {
 	ok := s.host.Unmount()
 	if !ok {
 		//TODO try to force unmount if possible
-		log.Error().Str("path", s.path).Msg("unmount failed")
+		log.Error("unmount failed")
 	}
 }
diff --git a/src/export/fuse/mount.go b/src/export/fuse/mount.go
index a598612..e6590be 100644
--- a/src/export/fuse/mount.go
+++ b/src/export/fuse/mount.go
@@ -5,26 +5,24 @@ package fuse
 import (
 	"errors"
 	"io"
+	"log/slog"
 	"math"
 	"os"
 	"sync"
 
 	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
 	"github.com/billziss-gh/cgofuse/fuse"
-
-	"github.com/rs/zerolog"
-	"github.com/rs/zerolog/log"
 )
 
 type fuseFS struct {
 	fuse.FileSystemBase
 	fh *fileHandler
 
-	log zerolog.Logger
+	log *slog.Logger
 }
 
 func newFuseFS(fs vfs.Filesystem) fuse.FileSystemInterface {
-	l := log.Logger.With().Str("component", "fuse").Logger()
+	l := slog.With("component", "fuse")
 	return &fuseFS{
 		fh:  &fileHandler{fs: fs},
 		log: l,
@@ -32,14 +30,16 @@ func newFuseFS(fs vfs.Filesystem) fuse.FileSystemInterface {
 }
 
 func (fs *fuseFS) Open(path string, flags int) (errc int, fh uint64) {
+	log := fs.log.With("function", "Open", "path", path, "flags", flags)
+
 	fh, err := fs.fh.OpenHolder(path)
 	if os.IsNotExist(err) {
-		fs.log.Debug().Str("path", path).Msg("file does not exists")
+		log.Debug("file does not exists")
 		return -fuse.ENOENT, fhNone
 
 	}
 	if err != nil {
-		fs.log.Error().Err(err).Str("path", path).Msg("error opening file")
+		log.Error("error opening file", "err", err)
 		return -fuse.EIO, fhNone
 	}
 
@@ -57,6 +57,7 @@ func (fs *fuseFS) Opendir(path string) (errc int, fh uint64) {
 }
 
 func (fs *fuseFS) Getattr(path string, stat *fuse.Stat_t, fh uint64) (errc int) {
+	log := fs.log.With("function", "Getattr", "path", path, "filehandler", fh)
 	if path == "/" {
 		stat.Mode = fuse.S_IFDIR | 0555
 		return 0
@@ -64,12 +65,12 @@ func (fs *fuseFS) Getattr(path string, stat *fuse.Stat_t, fh uint64) (errc int)
 
 	file, err := fs.fh.GetFile(path, fh)
 	if os.IsNotExist(err) {
-		fs.log.Debug().Str("path", path).Msg("file does not exists")
+		log.Debug("file does not exists", "error", err)
 		return -fuse.ENOENT
 
 	}
 	if err != nil {
-		fs.log.Error().Err(err).Str("path", path).Msg("error getting holder when reading file attributes")
+		log.Error("error getting holder when reading file attributes", "error", err)
 		return -fuse.EIO
 	}
 
@@ -84,14 +85,15 @@ func (fs *fuseFS) Getattr(path string, stat *fuse.Stat_t, fh uint64) (errc int)
 }
 
 func (fs *fuseFS) Read(path string, dest []byte, off int64, fh uint64) int {
+	log := fs.log.With("function", "Read", "path", path, "offset", off, "filehandler", fh)
 	file, err := fs.fh.GetFile(path, fh)
 	if os.IsNotExist(err) {
-		fs.log.Error().Err(err).Str("path", path).Msg("file not found on READ operation")
+		log.Error("file not found on READ operation", "path", path, "error", err)
 		return -fuse.ENOENT
 
 	}
 	if err != nil {
-		fs.log.Error().Err(err).Str("path", path).Msg("error getting holder reading data from file")
+		fs.log.Error("error getting holder reading data from file", "path", path, "error", err)
 		return -fuse.EIO
 	}
 
@@ -104,7 +106,7 @@ func (fs *fuseFS) Read(path string, dest []byte, off int64, fh uint64) int {
 
 	n, err := file.ReadAt(buf, off)
 	if err != nil && err != io.EOF {
-		log.Error().Err(err).Str("path", path).Msg("error reading data")
+		log.Error("error reading data")
 		return -fuse.EIO
 	}
 
@@ -113,8 +115,9 @@ func (fs *fuseFS) Read(path string, dest []byte, off int64, fh uint64) int {
 }
 
 func (fs *fuseFS) Release(path string, fh uint64) int {
+	log := fs.log.With("function", "Release", "path", path, "filehandler", fh)
 	if err := fs.fh.Remove(fh); err != nil {
-		fs.log.Error().Err(err).Str("path", path).Msg("error getting holder when releasing file")
+		log.Error("error getting holder when releasing file", "path", path, "error", err)
 		return -fuse.EIO
 	}
 
@@ -129,19 +132,20 @@ func (fs *fuseFS) Readdir(path string,
 	fill func(name string, stat *fuse.Stat_t, ofst int64) bool,
 	ofst int64,
 	fh uint64) (errc int) {
+	log := fs.log.With("function", "Readdir", "path", path, "offset", ofst, "filehandler", fh)
 	fill(".", nil, 0)
 	fill("..", nil, 0)
 
 	//TODO improve this function to make use of fh index if possible
 	paths, err := fs.fh.ListDir(path)
 	if err != nil {
-		fs.log.Error().Err(err).Str("path", path).Msg("error reading directory")
+		log.Error("error reading directory", "error", err)
 		return -fuse.ENOSYS
 	}
 
 	for _, p := range paths {
 		if !fill(p, nil, 0) {
-			fs.log.Error().Str("path", path).Msg("error adding directory")
+			log.Error("error adding directory")
 			break
 		}
 	}
diff --git a/src/export/nfs/handler.go b/src/export/nfs/handler.go
index 4c2acee..c722fb4 100644
--- a/src/export/nfs/handler.go
+++ b/src/export/nfs/handler.go
@@ -1,15 +1,16 @@
 package nfs
 
 import (
+	"log/slog"
+
 	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
 	"git.kmsign.ru/royalcat/tstor/src/log"
-	zlog "github.com/rs/zerolog/log"
 	nfs "github.com/willscott/go-nfs"
 	nfshelper "github.com/willscott/go-nfs/helpers"
 )
 
 func NewNFSv3Handler(fs vfs.Filesystem) (nfs.Handler, error) {
-	nfslog := zlog.Logger.With().Str("component", "nfs").Logger()
+	nfslog := slog.With("component", "nfs")
 	nfs.SetLogger(log.NewNFSLog(nfslog))
 	nfs.Log.SetLevel(nfs.InfoLevel)
 
diff --git a/src/export/nfs/wrapper-v3.go b/src/export/nfs/wrapper-v3.go
index e855221..fc4f6b7 100644
--- a/src/export/nfs/wrapper-v3.go
+++ b/src/export/nfs/wrapper-v3.go
@@ -3,16 +3,16 @@ package nfs
 import (
 	"errors"
 	"io/fs"
+	"log/slog"
 	"path/filepath"
 
 	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
 	"github.com/go-git/go-billy/v5"
-	"github.com/rs/zerolog"
 )
 
 type billyFsWrapper struct {
 	fs  vfs.Filesystem
-	log zerolog.Logger
+	log *slog.Logger
 }
 
 var _ billy.Filesystem = (*billyFsWrapper)(nil)
@@ -56,7 +56,7 @@ func (fs *billyFsWrapper) Open(filename string) (billy.File, error) {
 	return &billyFile{
 		name: filename,
 		file: file,
-		log:  fs.log.With().Str("filename", filename).Logger(),
+		log:  fs.log.With("filename", filename),
 	}, nil
 }
 
@@ -69,7 +69,7 @@ func (fs *billyFsWrapper) OpenFile(filename string, flag int, perm fs.FileMode)
 	return &billyFile{
 		name: filename,
 		file: file,
-		log:  fs.log.With().Str("filename", filename).Int("flag", flag).Str("perm", perm.String()).Logger(),
+		log:  fs.log.With("filename", filename, "flag", flag, "perm", perm.String()),
 	}, nil
 }
 
@@ -102,8 +102,8 @@ func (*billyFsWrapper) Readlink(link string) (string, error) {
 }
 
 // Remove implements billy.Filesystem.
-func (*billyFsWrapper) Remove(filename string) error {
-	return billy.ErrNotSupported
+func (s *billyFsWrapper) Remove(filename string) error {
+	return s.fs.Unlink(filename)
 }
 
 // Rename implements billy.Filesystem.
@@ -138,7 +138,7 @@ func (fs *billyFsWrapper) TempFile(dir string, prefix string) (billy.File, error
 type billyFile struct {
 	name string
 	file vfs.File
-	log  zerolog.Logger
+	log  *slog.Logger
 }
 
 var _ billy.File = (*billyFile)(nil)
@@ -188,13 +188,13 @@ func (*billyFile) Unlock() error {
 	return nil // TODO
 }
 
-func billyErr(err error, log zerolog.Logger) error {
+func billyErr(err error, log *slog.Logger) error {
 	if errors.Is(err, vfs.ErrNotImplemented) {
 		return billy.ErrNotSupported
 	}
 	if errors.Is(err, vfs.ErrNotExist) {
 		if err, ok := asErr[*fs.PathError](err); ok {
-			log.Error().Err(err.Err).Str("op", err.Op).Str("path", err.Path).Msg("file not found")
+			log.Error("file not found", "op", err.Op, "path", err.Path, "error", err.Err)
 		}
 		return fs.ErrNotExist
 	}
diff --git a/src/export/webdav/handler.go b/src/export/webdav/handler.go
index 3c478ce..c44528b 100644
--- a/src/export/webdav/handler.go
+++ b/src/export/webdav/handler.go
@@ -1,22 +1,22 @@
 package webdav
 
 import (
+	"log/slog"
 	"net/http"
 
 	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
-	"github.com/rs/zerolog/log"
 	"golang.org/x/net/webdav"
 )
 
 func newHandler(fs vfs.Filesystem) *webdav.Handler {
-	l := log.Logger.With().Str("component", "webDAV").Logger()
+	log := slog.With("component", "webDAV")
 	return &webdav.Handler{
 		Prefix:     "/",
 		FileSystem: newFS(fs),
 		LockSystem: webdav.NewMemLS(),
 		Logger: func(req *http.Request, err error) {
 			if err != nil {
-				l.Error().Err(err).Str("path", req.RequestURI).Msg("webDAV error")
+				log.Error("webDAV error", "path", req.RequestURI, "error", err)
 			}
 		},
 	}
diff --git a/src/export/webdav/http.go b/src/export/webdav/http.go
index c7d2f0d..f88336b 100644
--- a/src/export/webdav/http.go
+++ b/src/export/webdav/http.go
@@ -2,10 +2,10 @@ package webdav
 
 import (
 	"fmt"
+	"log/slog"
 	"net/http"
 
 	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
-	"github.com/rs/zerolog/log"
 	"golang.org/x/net/webdav"
 )
 
@@ -33,21 +33,21 @@ func NewWebDAVServer(fs vfs.Filesystem, port int, user, pass string) error {
 		Handler: serveMux,
 	}
 
-	log.Info().Str("host", httpServer.Addr).Msg("starting webDAV server")
+	slog.With("host", httpServer.Addr).Info("starting webDAV server")
 
 	return httpServer.ListenAndServe()
 }
 
 func NewDirServer(dir string, port int, user, pass string) error {
 
-	l := log.Logger.With().Str("component", "webDAV").Logger()
+	log := slog.With("component", "webDAV")
 	srv := &webdav.Handler{
 		Prefix:     "/",
 		FileSystem: webdav.Dir(dir),
 		LockSystem: webdav.NewMemLS(),
 		Logger: func(req *http.Request, err error) {
 			if err != nil {
-				l.Error().Err(err).Str("path", req.RequestURI).Msg("webDAV error")
+				log.Error("webDAV error", "path", req.RequestURI)
 			}
 		},
 	}
@@ -72,7 +72,7 @@ func NewDirServer(dir string, port int, user, pass string) error {
 		Handler: serveMux,
 	}
 
-	log.Info().Str("host", httpServer.Addr).Msg("starting webDAV server")
+	log.Info("starting webDAV server", "host", httpServer.Addr)
 
 	return httpServer.ListenAndServe()
 }
diff --git a/src/host/controller/torrent.go b/src/host/controller/torrent.go
new file mode 100644
index 0000000..3a04b41
--- /dev/null
+++ b/src/host/controller/torrent.go
@@ -0,0 +1,107 @@
+package controller
+
+import (
+	"slices"
+	"strings"
+
+	"git.kmsign.ru/royalcat/tstor/src/host/store"
+	"github.com/anacrolix/torrent"
+)
+
+type Torrent struct {
+	torrentFilePath string
+	t               *torrent.Torrent
+	rep             *store.ExlcudedFiles
+}
+
+func NewTorrent(t *torrent.Torrent, rep *store.ExlcudedFiles) *Torrent {
+	return &Torrent{t: t, rep: rep}
+}
+
+func (s *Torrent) TorrentFilePath() string {
+	return s.torrentFilePath
+}
+
+func (s *Torrent) Torrent() *torrent.Torrent {
+	return s.t
+}
+
+func (s *Torrent) Name() string {
+	<-s.t.GotInfo()
+	return s.t.Name()
+}
+
+func (s *Torrent) InfoHash() string {
+	<-s.t.GotInfo()
+	return s.t.InfoHash().HexString()
+}
+
+func (s *Torrent) BytesCompleted() int64 {
+	<-s.t.GotInfo()
+	return s.t.BytesCompleted()
+}
+
+func (s *Torrent) BytesMissing() int64 {
+	<-s.t.GotInfo()
+	return s.t.BytesMissing()
+}
+
+func (s *Torrent) Files() ([]*torrent.File, error) {
+	excludedFiles, err := s.rep.ExcludedFiles(s.t.InfoHash())
+	if err != nil {
+		return nil, err
+	}
+
+	<-s.t.GotInfo()
+	files := s.t.Files()
+	files = slices.DeleteFunc(files, func(file *torrent.File) bool {
+		p := file.Path()
+
+		if strings.Contains(p, "/.pad/") {
+			return false
+		}
+
+		if !slices.Contains(excludedFiles, p) {
+			return false
+		}
+
+		return true
+	})
+
+	return files, nil
+}
+
+func (s *Torrent) ExcludedFiles() ([]*torrent.File, error) {
+	excludedFiles, err := s.rep.ExcludedFiles(s.t.InfoHash())
+	if err != nil {
+		return nil, err
+	}
+
+	<-s.t.GotInfo()
+	files := s.t.Files()
+	files = slices.DeleteFunc(files, func(file *torrent.File) bool {
+		p := file.Path()
+
+		if strings.Contains(p, "/.pad/") {
+			return false
+		}
+
+		if slices.Contains(excludedFiles, p) {
+			return false
+		}
+
+		return true
+	})
+
+	return files, nil
+}
+
+func (s *Torrent) ExcludeFile(f *torrent.File) error {
+	return s.rep.ExcludeFile(f)
+}
+
+func (s *Torrent) ValidateTorrent() error {
+	<-s.t.GotInfo()
+	s.t.VerifyData()
+	return nil
+}
diff --git a/src/host/storage/storage.go b/src/host/filestorage/setup.go
similarity index 88%
rename from src/host/storage/storage.go
rename to src/host/filestorage/setup.go
index a400426..17e5448 100644
--- a/src/host/storage/storage.go
+++ b/src/host/filestorage/setup.go
@@ -1,4 +1,4 @@
-package storage
+package filestorage
 
 import (
 	"fmt"
@@ -6,15 +6,16 @@ import (
 	"path/filepath"
 
 	"git.kmsign.ru/royalcat/tstor/src/config"
+	"git.kmsign.ru/royalcat/tstor/src/host/store"
 	"github.com/anacrolix/torrent/storage"
 )
 
-func SetupStorage(cfg config.TorrentClient) (storage.ClientImplCloser, storage.PieceCompletion, error) {
+func Setup(cfg config.TorrentClient) (*FileStorage, 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)
 	}
-	pc, err := NewBadgerPieceCompletion(pcp)
+	pc, err := store.NewBadgerPieceCompletion(pcp)
 	if err != nil {
 		return nil, nil, fmt.Errorf("error creating servers piece completion: %w", err)
 	}
diff --git a/src/host/filestorage/storage_files.go b/src/host/filestorage/storage_files.go
new file mode 100644
index 0000000..8b3fa8c
--- /dev/null
+++ b/src/host/filestorage/storage_files.go
@@ -0,0 +1,186 @@
+package filestorage
+
+import (
+	"context"
+	"log/slog"
+	"os"
+	"path"
+	"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"
+)
+
+type FileStorageDeleter interface {
+	storage.ClientImplCloser
+	DeleteFile(file *torrent.File) error
+}
+
+// NewFileStorage creates a new ClientImplCloser that stores files using the OS native filesystem.
+func NewFileStorage(baseDir string, pc storage.PieceCompletion) *FileStorage {
+	return &FileStorage{
+		baseDir: baseDir,
+		ClientImplCloser: storage.NewFileOpts(storage.NewFileClientOpts{
+			ClientBaseDir:   baseDir,
+			PieceCompletion: pc,
+			TorrentDirMaker: torrentDir,
+			FilePathMaker:   filePath,
+		}),
+		pieceCompletion: pc,
+		log:             slog.With("component", "torrent-client"),
+	}
+}
+
+// File-based storage for torrents, that isn't yet bound to a particular torrent.
+type FileStorage struct {
+	baseDir string
+	storage.ClientImplCloser
+	pieceCompletion storage.PieceCompletion
+	log             *slog.Logger
+}
+
+func (me *FileStorage) Close() error {
+	return me.pieceCompletion.Close()
+}
+
+func torrentDir(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string {
+	return filepath.Join(baseDir, info.Name)
+}
+
+func filePath(opts storage.FilePathMakerOpts) string {
+	return filepath.Join(opts.File.Path...)
+}
+
+func (fs *FileStorage) filePath(info *metainfo.Info, infoHash metainfo.Hash, fileInfo *metainfo.FileInfo) string {
+	return filepath.Join(torrentDir(fs.baseDir, info, infoHash), filePath(storage.FilePathMakerOpts{
+		Info: info,
+		File: fileInfo,
+	}))
+}
+
+func (fs *FileStorage) DeleteFile(file *torrent.File) error {
+	info := file.Torrent().Info()
+	infoHash := file.Torrent().InfoHash()
+	torrentDir := torrentDir(fs.baseDir, info, infoHash)
+	fileInfo := file.FileInfo()
+	relFilePath := filePath(storage.FilePathMakerOpts{
+		Info: info,
+		File: &fileInfo,
+	})
+	filePath := path.Join(torrentDir, relFilePath)
+	for i := file.BeginPieceIndex(); i < file.EndPieceIndex(); i++ {
+		pk := metainfo.PieceKey{InfoHash: infoHash, Index: i}
+		err := fs.pieceCompletion.Set(pk, false)
+		if err != nil {
+			return err
+		}
+	}
+	return os.Remove(filePath)
+}
+
+func (fs *FileStorage) CleanupDirs(ctx context.Context, expected []*controller.Torrent, dryRun bool) (int, error) {
+	log := fs.log.With("function", "CleanupDirs", "expectedTorrents", len(expected), "dryRun", dryRun)
+
+	expectedEntries := []string{}
+	for _, e := range expected {
+		expectedEntries = append(expectedEntries, e.Torrent().Name())
+	}
+
+	entries, err := os.ReadDir(fs.baseDir)
+	if err != nil {
+		return 0, err
+	}
+
+	toDelete := []string{}
+	for _, v := range entries {
+		if !slices.Contains(expectedEntries, v.Name()) {
+			toDelete = append(toDelete, v.Name())
+		}
+	}
+
+	if ctx.Err() != nil {
+		return 0, ctx.Err()
+	}
+
+	log.Info("deleting trash data", "dirsCount", len(toDelete))
+	if !dryRun {
+		for i, name := range toDelete {
+			p := path.Join(fs.baseDir, name)
+			log.Warn("deleting trash data", "path", p)
+			err := os.RemoveAll(p)
+			if err != nil {
+				return i, err
+			}
+		}
+	}
+
+	return len(toDelete), nil
+}
+
+func (fs *FileStorage) CleanupFiles(ctx context.Context, expected []*controller.Torrent, dryRun bool) (int, error) {
+	log := fs.log.With("function", "CleanupFiles", "expectedTorrents", len(expected), "dryRun", dryRun)
+
+	expectedEntries := []string{}
+	{
+		for _, e := range expected {
+			files, err := e.Files()
+			if err != nil {
+				return 0, err
+			}
+
+			for _, f := range files {
+				expectedEntries = append(expectedEntries, fs.filePath(e.Torrent().Info(), e.Torrent().InfoHash(), ptr(f.FileInfo())))
+			}
+		}
+	}
+
+	entries := []string{}
+	err := filepath.Walk(fs.baseDir,
+		func(path string, info os.FileInfo, err error) error {
+			if err != nil {
+				return err
+			}
+			if ctx.Err() != nil {
+				return ctx.Err()
+			}
+
+			if info.IsDir() {
+				return nil
+			}
+			entries = append(entries, path)
+			return nil
+		})
+	if err != nil {
+		return 0, err
+	}
+
+	toDelete := []string{}
+	for _, v := range entries {
+		if !slices.Contains(expectedEntries, v) {
+			toDelete = append(toDelete, v)
+		}
+	}
+
+	if ctx.Err() != nil {
+		return len(toDelete), ctx.Err()
+	}
+
+	log.Info("deleting trash data", "filesCount", len(toDelete))
+	if !dryRun {
+		for i, p := range toDelete {
+			fs.log.Warn("deleting trash data", "path", p)
+			err := os.Remove(p)
+			if err != nil {
+				return i, err
+			}
+		}
+	}
+	return len(toDelete), nil
+}
+
+func ptr[D any](v D) *D {
+	return &v
+}
diff --git a/src/host/service/service.go b/src/host/service/service.go
index e48f80a..d99cb1a 100644
--- a/src/host/service/service.go
+++ b/src/host/service/service.go
@@ -4,99 +4,153 @@ import (
 	"context"
 	"fmt"
 	"log/slog"
+	"os"
+	"path/filepath"
+	"strings"
 	"time"
 
-	"git.kmsign.ru/royalcat/tstor/src/host/storage"
+	"git.kmsign.ru/royalcat/tstor/src/host/controller"
+	"git.kmsign.ru/royalcat/tstor/src/host/filestorage"
+	"git.kmsign.ru/royalcat/tstor/src/host/store"
 	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
+
 	"github.com/anacrolix/torrent"
+	"github.com/anacrolix/torrent/bencode"
 	"github.com/anacrolix/torrent/metainfo"
 	"github.com/anacrolix/torrent/types"
+	"github.com/anacrolix/torrent/types/infohash"
 )
 
 type Service struct {
-	c   *torrent.Client
-	rep storage.ExlcudedFiles
+	c             *torrent.Client
+	excludedFiles *store.ExlcudedFiles
+	infoBytes     *store.InfoBytes
+
+	torrentLoaded chan struct{}
 
 	// stats *Stats
 	DefaultPriority types.PiecePriority
+	Storage         *filestorage.FileStorage
+	SourceDir       string
 
 	log                     *slog.Logger
 	addTimeout, readTimeout int
 }
 
-func NewService(c *torrent.Client, rep storage.ExlcudedFiles, addTimeout, readTimeout int) *Service {
-	l := slog.With("component", "torrent-service")
-	return &Service{
-		log:             l,
+func NewService(sourceDir string, c *torrent.Client, storage *filestorage.FileStorage, excludedFiles *store.ExlcudedFiles, infoBytes *store.InfoBytes, addTimeout, readTimeout int) *Service {
+	s := &Service{
+		log:             slog.With("component", "torrent-service"),
 		c:               c,
 		DefaultPriority: types.PiecePriorityNone,
-		rep:             rep,
+		excludedFiles:   excludedFiles,
+		infoBytes:       infoBytes,
+		Storage:         storage,
+		SourceDir:       sourceDir,
+		torrentLoaded:   make(chan struct{}),
 		// stats:       newStats(), // TODO persistent
 		addTimeout:  addTimeout,
 		readTimeout: readTimeout,
 	}
+
+	go func() {
+		err := s.loadTorrentFiles(context.Background())
+		if err != nil {
+			s.log.Error("initial torrent load failed", "error", err)
+		}
+		close(s.torrentLoaded)
+	}()
+
+	return s
 }
 
 var _ vfs.FsFactory = (*Service)(nil).NewTorrentFs
 
-func (s *Service) NewTorrent(f vfs.File) (*torrent.Torrent, error) {
-	ctx, cancel := context.WithTimeout(context.TODO(), time.Second*time.Duration(s.addTimeout))
-	defer cancel()
+func (s *Service) AddTorrent(ctx context.Context, f vfs.File) (*torrent.Torrent, error) {
 	defer f.Close()
 
+	stat, err := f.Stat()
+	if err != nil {
+		return nil, fmt.Errorf("call stat failed: %w", err)
+	}
+
 	mi, err := metainfo.Load(f)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("loading torrent metadata from file %s, error: %w", stat.Name(), err)
 	}
-
 	t, ok := s.c.Torrent(mi.HashInfoBytes())
 	if !ok {
-		t, err = s.c.AddTorrent(mi)
+		spec, err := torrent.TorrentSpecFromMetaInfoErr(mi)
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("parse spec from metadata: %w", err)
 		}
+		infoBytes := spec.InfoBytes
+
+		if !isValidInfoHashBytes(infoBytes) {
+			infoBytes = nil
+		}
+
+		if len(infoBytes) == 0 {
+			infoBytes, err = s.infoBytes.GetBytes(spec.InfoHash)
+			if err != nil && err != store.ErrNotFound {
+				return nil, fmt.Errorf("get info bytes from database: %w", err)
+			}
+		}
+
+		var info metainfo.Info
+		err = bencode.Unmarshal(infoBytes, &info)
+		if err != nil {
+			infoBytes = nil
+		} else {
+			for _, t := range s.c.Torrents() {
+				if t.Name() == info.BestName() {
+					return nil, fmt.Errorf("torrent with name '%s' already exists", t.Name())
+				}
+			}
+		}
+
+		t, _ = s.c.AddTorrentOpt(torrent.AddTorrentOpts{
+			InfoHash:  spec.InfoHash,
+			Storage:   s.Storage,
+			InfoBytes: infoBytes,
+			ChunkSize: spec.ChunkSize,
+		})
+		t.AllowDataDownload()
+
 		select {
 		case <-ctx.Done():
-			return nil, fmt.Errorf("creating torrent fs timed out")
+			return nil, fmt.Errorf("creating torrent timed out")
 		case <-t.GotInfo():
+			err := s.infoBytes.Set(t.InfoHash(), t.Metainfo())
+			if err != nil {
+				s.log.Error("error setting info bytes for torrent %s: %s", t.Name(), err.Error())
+			}
+			for _, f := range t.Files() {
+				f.SetPriority(s.DefaultPriority)
+			}
+
 		}
-		for _, f := range t.Files() {
-			f.SetPriority(s.DefaultPriority)
-		}
-		t.AllowDataDownload()
 	}
 
 	return t, nil
 }
 
+func isValidInfoHashBytes(d []byte) bool {
+	var info metainfo.Info
+	err := bencode.Unmarshal(d, &info)
+	return err == nil
+}
+
 func (s *Service) NewTorrentFs(f vfs.File) (vfs.Filesystem, error) {
 	ctx, cancel := context.WithTimeout(context.TODO(), time.Second*time.Duration(s.addTimeout))
 	defer cancel()
 	defer f.Close()
 
-	mi, err := metainfo.Load(f)
+	t, err := s.AddTorrent(ctx, f)
 	if err != nil {
 		return nil, err
 	}
 
-	t, ok := s.c.Torrent(mi.HashInfoBytes())
-	if !ok {
-		t, err = s.c.AddTorrent(mi)
-		if err != nil {
-			return nil, err
-		}
-		select {
-		case <-ctx.Done():
-			return nil, fmt.Errorf("creating torrent fs timed out")
-		case <-t.GotInfo():
-		}
-		for _, f := range t.Files() {
-			f.SetPriority(s.DefaultPriority)
-		}
-		t.AllowDataDownload()
-	}
-
-	return vfs.NewTorrentFs(t, s.rep, s.readTimeout), nil
+	return vfs.NewTorrentFs(controller.NewTorrent(t, s.excludedFiles), s.readTimeout), nil
 }
 
 func (s *Service) Stats() (*Stats, error) {
@@ -106,3 +160,52 @@ func (s *Service) Stats() (*Stats, error) {
 func (s *Service) GetStats() torrent.ConnStats {
 	return s.c.ConnStats()
 }
+
+func (s *Service) loadTorrentFiles(ctx context.Context) error {
+	return filepath.Walk(s.SourceDir, func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			return fmt.Errorf("fs walk error: %w", err)
+		}
+
+		if ctx.Err() != nil {
+			return ctx.Err()
+		}
+
+		if info.IsDir() {
+			return nil
+		}
+
+		if strings.HasSuffix(path, ".torrent") {
+			file := vfs.NewLazyOsFile(path)
+			defer file.Close()
+
+			_, err = s.AddTorrent(ctx, file)
+			if err != nil {
+				s.log.Error("failed adding torrent", "error", err)
+			}
+		}
+
+		return nil
+	})
+}
+
+func (s *Service) ListTorrents(ctx context.Context) ([]*controller.Torrent, error) {
+	<-s.torrentLoaded
+
+	out := []*controller.Torrent{}
+	for _, v := range s.c.Torrents() {
+		out = append(out, controller.NewTorrent(v, s.excludedFiles))
+	}
+	return out, nil
+}
+
+func (s *Service) GetTorrent(infohashHex string) (*controller.Torrent, error) {
+	<-s.torrentLoaded
+
+	t, ok := s.c.Torrent(infohash.FromHexString(infohashHex))
+	if !ok {
+		return nil, nil
+	}
+
+	return controller.NewTorrent(t, s.excludedFiles), nil
+}
diff --git a/src/host/storage/client.go b/src/host/storage/client.go
deleted file mode 100644
index bacdc15..0000000
--- a/src/host/storage/client.go
+++ /dev/null
@@ -1,36 +0,0 @@
-package storage
-
-import (
-	"time"
-
-	"github.com/anacrolix/dht/v2"
-	"github.com/anacrolix/dht/v2/bep44"
-	tlog "github.com/anacrolix/log"
-	"github.com/anacrolix/torrent"
-	"github.com/anacrolix/torrent/storage"
-	"github.com/rs/zerolog/log"
-
-	"git.kmsign.ru/royalcat/tstor/src/config"
-	dlog "git.kmsign.ru/royalcat/tstor/src/log"
-)
-
-func NewClient(st storage.ClientImpl, fis bep44.Store, cfg *config.TorrentClient, id [20]byte) (*torrent.Client, error) {
-	// TODO download and upload limits
-	torrentCfg := torrent.NewDefaultClientConfig()
-	torrentCfg.PeerID = string(id[:])
-	torrentCfg.DefaultStorage = st
-
-	l := log.Logger.With().Str("component", "torrent-client").Logger()
-
-	tl := tlog.NewLogger()
-	tl.SetHandlers(&dlog.Torrent{L: l})
-	torrentCfg.Logger = tl
-
-	torrentCfg.ConfigureAnacrolixDhtServer = func(cfg *dht.ServerConfig) {
-		cfg.Store = fis
-		cfg.Exp = 2 * time.Hour
-		cfg.NoSecurity = false
-	}
-
-	return torrent.NewClient(torrentCfg)
-}
diff --git a/src/host/storage/storage_files.go b/src/host/storage/storage_files.go
deleted file mode 100644
index 52e3515..0000000
--- a/src/host/storage/storage_files.go
+++ /dev/null
@@ -1,105 +0,0 @@
-package storage
-
-import (
-	"os"
-	"path"
-	"path/filepath"
-	"slices"
-
-	"github.com/anacrolix/torrent"
-	"github.com/anacrolix/torrent/metainfo"
-	"github.com/anacrolix/torrent/storage"
-	"github.com/rs/zerolog"
-	"github.com/rs/zerolog/log"
-)
-
-type FileStorageDeleter interface {
-	storage.ClientImplCloser
-	DeleteFile(file *torrent.File) error
-	Cleanup(expected []*torrent.Torrent) error
-}
-
-// NewFileStorage creates a new ClientImplCloser that stores files using the OS native filesystem.
-func NewFileStorage(baseDir string, pc storage.PieceCompletion) FileStorageDeleter {
-
-	return &FileStorage{
-		baseDir: baseDir,
-		ClientImplCloser: storage.NewFileOpts(storage.NewFileClientOpts{
-			ClientBaseDir:   baseDir,
-			PieceCompletion: pc,
-			TorrentDirMaker: torrentDir,
-			FilePathMaker: func(opts storage.FilePathMakerOpts) string {
-				return filePath(opts.File)
-			},
-		}),
-		pieceCompletion: pc,
-		log:             log.Logger.With().Str("component", "torrent-client").Logger(),
-	}
-}
-
-// File-based storage for torrents, that isn't yet bound to a particular torrent.
-type FileStorage struct {
-	baseDir string
-	storage.ClientImplCloser
-	pieceCompletion storage.PieceCompletion
-	log             zerolog.Logger
-}
-
-func (me *FileStorage) Close() error {
-	return me.pieceCompletion.Close()
-}
-
-func torrentDir(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string {
-	return filepath.Join(baseDir, info.Name)
-}
-
-func filePath(file *metainfo.FileInfo) string {
-	return filepath.Join(file.Path...)
-}
-
-func (fs *FileStorage) DeleteFile(file *torrent.File) error {
-	info := file.Torrent().Info()
-	infoHash := file.Torrent().InfoHash()
-	torrentDir := torrentDir(fs.baseDir, info, infoHash)
-	fileInfo := file.FileInfo()
-	relFilePath := filePath(&fileInfo)
-	filePath := path.Join(torrentDir, relFilePath)
-	for i := file.BeginPieceIndex(); i < file.EndPieceIndex(); i++ {
-		pk := metainfo.PieceKey{InfoHash: infoHash, Index: i}
-		err := fs.pieceCompletion.Set(pk, false)
-		if err != nil {
-			return err
-		}
-	}
-	return os.Remove(filePath)
-}
-
-func (fs *FileStorage) Cleanup(expected []*torrent.Torrent) error {
-	expectedEntries := []string{}
-	for _, e := range expected {
-		expectedEntries = append(expectedEntries, e.Name())
-	}
-
-	entries, err := os.ReadDir(fs.baseDir)
-	if err != nil {
-		return err
-	}
-
-	toDelete := []string{}
-	for _, v := range entries {
-		if !slices.Contains(expectedEntries, v.Name()) {
-			toDelete = append(toDelete, v.Name())
-		}
-	}
-
-	fs.log.Info().Int("count", len(toDelete)).Msg("start deleting trash data")
-	for _, name := range toDelete {
-		p := path.Join(fs.baseDir, name)
-		fs.log.Info().Str("path", p).Msg("deleting trash data")
-		err := os.RemoveAll(p)
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}
diff --git a/src/host/store/client.go b/src/host/store/client.go
new file mode 100644
index 0000000..8275424
--- /dev/null
+++ b/src/host/store/client.go
@@ -0,0 +1,47 @@
+package store
+
+import (
+	"log/slog"
+
+	"github.com/anacrolix/dht/v2/bep44"
+	tlog "github.com/anacrolix/log"
+	"github.com/anacrolix/torrent"
+	"github.com/anacrolix/torrent/storage"
+
+	"git.kmsign.ru/royalcat/tstor/src/config"
+	dlog "git.kmsign.ru/royalcat/tstor/src/log"
+)
+
+// MOVE
+func NewClient(st storage.ClientImpl, fis bep44.Store, cfg *config.TorrentClient, id [20]byte) (*torrent.Client, error) {
+	l := slog.With("component", "torrent-client")
+
+	// TODO download and upload limits
+	torrentCfg := torrent.NewDefaultClientConfig()
+	torrentCfg.PeerID = string(id[:])
+	torrentCfg.DefaultStorage = st
+	// torrentCfg.AlwaysWantConns = true
+	// torrentCfg.DisableAggressiveUpload = true
+	// torrentCfg.Seed = true
+	// torrentCfg.DownloadRateLimiter = rate.NewLimiter(rate.Inf, 0)
+	// torrentCfg
+
+	tl := tlog.NewLogger()
+	tl.SetHandlers(&dlog.Torrent{L: l})
+	torrentCfg.Logger = tl
+	torrentCfg.Callbacks.NewPeer = append(torrentCfg.Callbacks.NewPeer, func(p *torrent.Peer) {
+		l.Debug("new peer", "ip", p.RemoteAddr.String())
+	})
+
+	torrentCfg.Callbacks.NewPeer = append(torrentCfg.Callbacks.PeerClosed, func(p *torrent.Peer) {
+		l.Debug("peer closed", "ip", p.RemoteAddr.String())
+	})
+
+	// torrentCfg.ConfigureAnacrolixDhtServer = func(cfg *dht.ServerConfig) {
+	// 	cfg.Store = fis
+	// 	cfg.Exp = 2 * time.Hour
+	// 	cfg.NoSecurity = false
+	// }
+
+	return torrent.NewClient(torrentCfg)
+}
diff --git a/src/host/storage/excluded-files.go b/src/host/store/excluded-files.go
similarity index 68%
rename from src/host/storage/excluded-files.go
rename to src/host/store/excluded-files.go
index 94a75e9..cdcd585 100644
--- a/src/host/storage/excluded-files.go
+++ b/src/host/store/excluded-files.go
@@ -1,4 +1,4 @@
-package storage
+package store
 
 import (
 	"errors"
@@ -7,18 +7,12 @@ import (
 
 	"github.com/anacrolix/torrent"
 	"github.com/anacrolix/torrent/metainfo"
-	atstorage "github.com/anacrolix/torrent/storage"
 	"github.com/philippgille/gokv"
 	"github.com/philippgille/gokv/badgerdb"
 	"github.com/philippgille/gokv/encoding"
 )
 
-type ExlcudedFiles interface {
-	ExcludeFile(file *torrent.File) error
-	ExcludedFiles(hash metainfo.Hash) ([]string, error)
-}
-
-func NewExcludedFiles(metaDir string, storage atstorage.ClientImplCloser) (ExlcudedFiles, error) {
+func NewExcludedFiles(metaDir string, storage TorrentFileDeleter) (*ExlcudedFiles, error) {
 	excludedFilesStore, err := badgerdb.NewStore(badgerdb.Options{
 		Dir:   filepath.Join(metaDir, "excluded-files"),
 		Codec: encoding.JSON,
@@ -28,7 +22,7 @@ func NewExcludedFiles(metaDir string, storage atstorage.ClientImplCloser) (Exlcu
 		return nil, err
 	}
 
-	r := &torrentRepositoryImpl{
+	r := &ExlcudedFiles{
 		excludedFiles: excludedFilesStore,
 		storage:       storage,
 	}
@@ -36,15 +30,19 @@ func NewExcludedFiles(metaDir string, storage atstorage.ClientImplCloser) (Exlcu
 	return r, nil
 }
 
-type torrentRepositoryImpl struct {
+type ExlcudedFiles struct {
 	m             sync.RWMutex
 	excludedFiles gokv.Store
-	storage       atstorage.ClientImplCloser
+	storage       TorrentFileDeleter
 }
 
 var ErrNotFound = errors.New("not found")
 
-func (r *torrentRepositoryImpl) ExcludeFile(file *torrent.File) error {
+type TorrentFileDeleter interface {
+	DeleteFile(file *torrent.File) error
+}
+
+func (r *ExlcudedFiles) ExcludeFile(file *torrent.File) error {
 	r.m.Lock()
 	defer r.m.Unlock()
 
@@ -59,17 +57,15 @@ func (r *torrentRepositoryImpl) ExcludeFile(file *torrent.File) error {
 	}
 	excludedFiles = unique(append(excludedFiles, file.Path()))
 
-	if storage, ok := r.storage.(FileStorageDeleter); ok {
-		err = storage.DeleteFile(file)
-		if err != nil {
-			return err
-		}
+	err = r.storage.DeleteFile(file)
+	if err != nil {
+		return err
 	}
 
 	return r.excludedFiles.Set(hash.AsString(), excludedFiles)
 }
 
-func (r *torrentRepositoryImpl) ExcludedFiles(hash metainfo.Hash) ([]string, error) {
+func (r *ExlcudedFiles) ExcludedFiles(hash metainfo.Hash) ([]string, error) {
 	r.m.Lock()
 	defer r.m.Unlock()
 
diff --git a/src/host/storage/id.go b/src/host/store/id.go
similarity index 96%
rename from src/host/storage/id.go
rename to src/host/store/id.go
index 9422893..d8b59a7 100644
--- a/src/host/storage/id.go
+++ b/src/host/store/id.go
@@ -1,4 +1,4 @@
-package storage
+package store
 
 import (
 	"crypto/rand"
diff --git a/src/host/store/info.go b/src/host/store/info.go
new file mode 100644
index 0000000..44f5880
--- /dev/null
+++ b/src/host/store/info.go
@@ -0,0 +1,79 @@
+package store
+
+import (
+	"bytes"
+	"fmt"
+	"log/slog"
+	"path/filepath"
+
+	dlog "git.kmsign.ru/royalcat/tstor/src/log"
+	"github.com/anacrolix/torrent/metainfo"
+	"github.com/anacrolix/torrent/types/infohash"
+	"github.com/dgraph-io/badger/v4"
+)
+
+type InfoBytes struct {
+	db *badger.DB
+}
+
+func NewInfoBytes(metaDir string) (*InfoBytes, error) {
+	l := slog.With("component", "badger", "db", "info-bytes")
+
+	opts := badger.
+		DefaultOptions(filepath.Join(metaDir, "infobytes")).
+		WithLogger(&dlog.Badger{L: l})
+	db, err := badger.Open(opts)
+	if err != nil {
+		return nil, err
+	}
+	return &InfoBytes{db}, nil
+}
+
+func (k *InfoBytes) 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 fmt.Errorf("error getting value: %w", err)
+		}
+
+		data, err = item.ValueCopy(data)
+		return err
+	})
+	return data, err
+}
+
+func (k *InfoBytes) Get(ih infohash.T) (*metainfo.MetaInfo, error) {
+	data, err := k.GetBytes(ih)
+	if err != nil {
+		return nil, err
+	}
+
+	return metainfo.Load(bytes.NewReader(data))
+}
+
+func (me *InfoBytes) SetBytes(ih infohash.T, bytes []byte) error {
+	return me.db.Update(func(txn *badger.Txn) error {
+		return txn.Set(ih.Bytes(), bytes)
+	})
+}
+
+func (me *InfoBytes) Set(ih infohash.T, info metainfo.MetaInfo) error {
+	return me.db.Update(func(txn *badger.Txn) error {
+		return txn.Set(ih.Bytes(), info.InfoBytes)
+	})
+}
+
+func (k *InfoBytes) Delete(ih infohash.T) error {
+	return k.db.Update(func(txn *badger.Txn) error {
+		return txn.Delete(ih.Bytes())
+	})
+}
+
+func (me *InfoBytes) Close() error {
+	return me.db.Close()
+}
diff --git a/src/host/storage/piece-completion.go b/src/host/store/piece-completion.go
similarity index 95%
rename from src/host/storage/piece-completion.go
rename to src/host/store/piece-completion.go
index 7d727a0..61011bb 100644
--- a/src/host/storage/piece-completion.go
+++ b/src/host/store/piece-completion.go
@@ -1,4 +1,4 @@
-package storage
+package store
 
 import (
 	"encoding/binary"
@@ -9,7 +9,6 @@ import (
 	"github.com/anacrolix/torrent/metainfo"
 	"github.com/anacrolix/torrent/storage"
 	"github.com/dgraph-io/badger/v4"
-	"github.com/rs/zerolog/log"
 )
 
 type PieceCompletionState byte
@@ -34,7 +33,7 @@ type badgerPieceCompletion struct {
 var _ storage.PieceCompletion = (*badgerPieceCompletion)(nil)
 
 func NewBadgerPieceCompletion(dir string) (storage.PieceCompletion, error) {
-	l := log.Logger.With().Str("component", "badger").Str("db", "piece-completion").Logger()
+	l := slog.With("component", "badger", "db", "piece-completion")
 
 	opts := badger.
 		DefaultOptions(dir).
diff --git a/src/host/storage/store.go b/src/host/store/store.go
similarity index 94%
rename from src/host/storage/store.go
rename to src/host/store/store.go
index 0f9adf2..8192b67 100644
--- a/src/host/storage/store.go
+++ b/src/host/store/store.go
@@ -1,14 +1,14 @@
-package storage
+package store
 
 import (
 	"bytes"
 	"encoding/gob"
+	"log/slog"
 	"time"
 
 	dlog "git.kmsign.ru/royalcat/tstor/src/log"
 	"github.com/anacrolix/dht/v2/bep44"
 	"github.com/dgraph-io/badger/v4"
-	"github.com/rs/zerolog/log"
 )
 
 var _ bep44.Store = &FileItemStore{}
@@ -19,7 +19,7 @@ type FileItemStore struct {
 }
 
 func NewFileItemStore(path string, itemsTTL time.Duration) (*FileItemStore, error) {
-	l := log.Logger.With().Str("component", "item-store").Logger()
+	l := slog.With("component", "item-store")
 
 	opts := badger.DefaultOptions(path).
 		WithLogger(&dlog.Badger{L: l}).
diff --git a/src/host/vfs/archive.go b/src/host/vfs/archive.go
index f4e5649..d0708fa 100644
--- a/src/host/vfs/archive.go
+++ b/src/host/vfs/archive.go
@@ -8,7 +8,6 @@ import (
 	"path"
 	"path/filepath"
 	"strings"
-	"sync"
 
 	"git.kmsign.ru/royalcat/tstor/src/iio"
 	"github.com/bodgit/sevenzip"
@@ -17,34 +16,79 @@ import (
 
 var ArchiveFactories = map[string]FsFactory{
 	".zip": func(f File) (Filesystem, error) {
-		return NewArchive(f, f.Size(), ZipLoader), nil
+		stat, err := f.Stat()
+		if err != nil {
+			return nil, err
+		}
+		return NewArchive(stat.Name(), f, stat.Size(), ZipLoader), nil
 	},
 	".rar": func(f File) (Filesystem, error) {
-		return NewArchive(f, f.Size(), RarLoader), nil
+		stat, err := f.Stat()
+		if err != nil {
+			return nil, err
+		}
+		return NewArchive(stat.Name(), f, stat.Size(), RarLoader), nil
 	},
 	".7z": func(f File) (Filesystem, error) {
-		return NewArchive(f, f.Size(), SevenZipLoader), nil
+		stat, err := f.Stat()
+		if err != nil {
+			return nil, err
+		}
+		return NewArchive(stat.Name(), f, stat.Size(), SevenZipLoader), nil
 	},
 }
 
-type ArchiveLoader func(r iio.Reader, size int64) (map[string]*archiveFile, error)
+type archiveLoader func(r iio.Reader, size int64) (map[string]*archiveFile, error)
 
 var _ Filesystem = &archive{}
 
 type archive struct {
+	name string
+
 	r iio.Reader
 
 	size int64
 
-	files func() (map[string]*archiveFile, error)
+	files func() (map[string]File, error)
 }
 
-func NewArchive(r iio.Reader, size int64, loader ArchiveLoader) *archive {
+func NewArchive(name string, r iio.Reader, size int64, loader archiveLoader) *archive {
 	return &archive{
+		name: name,
 		r:    r,
 		size: size,
-		files: sync.OnceValues(func() (map[string]*archiveFile, error) {
-			return loader(r, size)
+		files: OnceValueWOErr(func() (map[string]File, error) {
+			zipFiles, err := loader(r, size)
+			if err != nil {
+				return nil, err
+			}
+			// TODO make optional
+			singleDir := true
+			for k := range zipFiles {
+				if !strings.HasPrefix(k, "/"+name+"/") {
+					singleDir = false
+					break
+				}
+			}
+
+			files := make(map[string]File, len(zipFiles))
+			for k, v := range zipFiles {
+				// TODO make optional
+				if strings.Contains(k, "/__MACOSX/") {
+					continue
+				}
+
+				if singleDir {
+					k, _ = strings.CutPrefix(k, "/"+name)
+				}
+
+				files[k] = v
+			}
+
+			// FIXME
+			files["/.forcegallery"] = NewMemoryFile(".forcegallery", []byte{})
+
+			return files, nil
 		}),
 	}
 }
@@ -80,7 +124,7 @@ func (afs *archive) Stat(filename string) (fs.FileInfo, error) {
 	}
 
 	if file, ok := files[filename]; ok {
-		return newFileInfo(path.Base(filename), file.Size()), nil
+		return file.Stat()
 	}
 
 	for p, _ := range files {
@@ -90,7 +134,6 @@ func (afs *archive) Stat(filename string) (fs.FileInfo, error) {
 	}
 
 	return nil, ErrNotExist
-
 }
 
 var _ File = &archiveFile{}
@@ -162,7 +205,7 @@ func (d *archiveFile) ReadAt(p []byte, off int64) (n int, err error) {
 	return d.reader.ReadAt(p, off)
 }
 
-var _ ArchiveLoader = ZipLoader
+var _ archiveLoader = ZipLoader
 
 func ZipLoader(reader iio.Reader, size int64) (map[string]*archiveFile, error) {
 	zr, err := zip.NewReader(reader, size)
@@ -171,14 +214,14 @@ func ZipLoader(reader iio.Reader, size int64) (map[string]*archiveFile, error) {
 	}
 
 	out := make(map[string]*archiveFile)
-	for _, f := range zr.File {
-		f := f
-		if f.FileInfo().IsDir() {
+	for i := range zr.File {
+		zipFile := zr.File[i]
+		if zipFile.FileInfo().IsDir() {
 			continue
 		}
 
 		rf := func() (iio.Reader, error) {
-			zr, err := f.Open()
+			zr, err := zipFile.Open()
 			if err != nil {
 				return nil, err
 			}
@@ -186,16 +229,13 @@ func ZipLoader(reader iio.Reader, size int64) (map[string]*archiveFile, error) {
 			return iio.NewDiskTeeReader(zr)
 		}
 
-		n := filepath.Join(string(os.PathSeparator), f.Name)
-		af := NewArchiveFile(f.Name, rf, f.FileInfo().Size())
-
-		out[n] = af
+		out[AbsPath(zipFile.Name)] = NewArchiveFile(zipFile.Name, rf, zipFile.FileInfo().Size())
 	}
 
 	return out, nil
 }
 
-var _ ArchiveLoader = SevenZipLoader
+var _ archiveLoader = SevenZipLoader
 
 func SevenZipLoader(reader iio.Reader, size int64) (map[string]*archiveFile, error) {
 	r, err := sevenzip.NewReader(reader, size)
@@ -228,7 +268,7 @@ func SevenZipLoader(reader iio.Reader, size int64) (map[string]*archiveFile, err
 	return out, nil
 }
 
-var _ ArchiveLoader = RarLoader
+var _ archiveLoader = RarLoader
 
 func RarLoader(reader iio.Reader, size int64) (map[string]*archiveFile, error) {
 	r, err := rardecode.NewReader(iio.NewSeekerWrapper(reader, size))
diff --git a/src/host/vfs/archive_test.go b/src/host/vfs/archive_test.go
index a984d92..c174438 100644
--- a/src/host/vfs/archive_test.go
+++ b/src/host/vfs/archive_test.go
@@ -18,7 +18,8 @@ func TestZipFilesystem(t *testing.T) {
 
 	zReader, size := createTestZip(require)
 
-	zfs := NewArchive(zReader, size, ZipLoader)
+	// TODO add single dir collapse test
+	zfs := NewArchive("test", zReader, size, ZipLoader)
 
 	files, err := zfs.ReadDir("/path/to/test/file")
 	require.NoError(err)
diff --git a/src/host/vfs/log.go b/src/host/vfs/log.go
new file mode 100644
index 0000000..0fb699d
--- /dev/null
+++ b/src/host/vfs/log.go
@@ -0,0 +1,117 @@
+package vfs
+
+import (
+	"io/fs"
+	"log/slog"
+)
+
+type LogFS struct {
+	fs  Filesystem
+	log *slog.Logger
+}
+
+var _ Filesystem = (*LogFS)(nil)
+
+func WrapLogFS(fs Filesystem, log *slog.Logger) *LogFS {
+	return &LogFS{
+		fs:  fs,
+		log: log.With("component", "fs"),
+	}
+}
+
+// Open implements Filesystem.
+func (fs *LogFS) Open(filename string) (File, error) {
+	file, err := fs.fs.Open(filename)
+	if err != nil {
+		fs.log.With("filename", filename).Error("Failed to open file")
+	}
+	file = WrapLogFile(file, filename, fs.log)
+	return file, err
+}
+
+// ReadDir implements Filesystem.
+func (fs *LogFS) ReadDir(path string) ([]fs.DirEntry, error) {
+	file, err := fs.fs.ReadDir(path)
+	if err != nil {
+		fs.log.Error("Failed to read dir", "path", path, "error", err)
+	}
+	return file, err
+}
+
+// Stat implements Filesystem.
+func (fs *LogFS) Stat(filename string) (fs.FileInfo, error) {
+	file, err := fs.fs.Stat(filename)
+	if err != nil {
+		fs.log.Error("Failed to stat", "filename", filename, "error", err)
+	}
+	return file, err
+}
+
+// Unlink implements Filesystem.
+func (fs *LogFS) Unlink(filename string) error {
+	err := fs.fs.Unlink(filename)
+	if err != nil {
+		fs.log.Error("Failed to stat", "filename", filename, "error", err)
+	}
+	return err
+}
+
+type LogFile struct {
+	f   File
+	log *slog.Logger
+}
+
+var _ File = (*LogFile)(nil)
+
+func WrapLogFile(f File, filename string, log *slog.Logger) *LogFile {
+	return &LogFile{
+		f:   f,
+		log: log.With("filename", filename),
+	}
+}
+
+// Close implements File.
+func (f *LogFile) Close() error {
+	err := f.f.Close()
+	if err != nil {
+		f.log.Error("Failed to close", "error", err)
+	}
+	return err
+}
+
+// IsDir implements File.
+func (f *LogFile) IsDir() bool {
+	return f.f.IsDir()
+}
+
+// Read implements File.
+func (f *LogFile) Read(p []byte) (n int, err error) {
+	n, err = f.f.Read(p)
+	if err != nil {
+		f.log.Error("Failed to read", "error", err)
+	}
+	return n, err
+}
+
+// ReadAt implements File.
+func (f *LogFile) ReadAt(p []byte, off int64) (n int, err error) {
+	n, err = f.f.ReadAt(p, off)
+	if err != nil {
+		f.log.Error("Failed to read", "offset", off, "error", err)
+	}
+	return n, err
+}
+
+// Size implements File.
+func (f *LogFile) Size() int64 {
+	return f.f.Size()
+}
+
+// Stat implements File.
+func (f *LogFile) Stat() (fs.FileInfo, error) {
+	info, err := f.f.Stat()
+	if err != nil {
+		f.log.Error("Failed to read", "error", err)
+	}
+	return info, err
+}
diff --git a/src/host/vfs/os.go b/src/host/vfs/os.go
index 658e64d..add84f3 100644
--- a/src/host/vfs/os.go
+++ b/src/host/vfs/os.go
@@ -31,17 +31,12 @@ func (fs *OsFS) Open(filename string) (File, error) {
 		return NewDir(filename), nil
 	}
 
-	osfile, err := os.Open(path.Join(fs.hostDir, filename))
-	if err != nil {
-		return nil, err
-	}
-	return NewOsFile(osfile), nil
+	return NewLazyOsFile(path.Join(fs.hostDir, filename)), nil
 }
 
 // ReadDir implements Filesystem.
 func (o *OsFS) ReadDir(dir string) ([]fs.DirEntry, error) {
-	dir = path.Join(o.hostDir, dir)
-	return os.ReadDir(dir)
+	return os.ReadDir(path.Join(o.hostDir, dir))
 }
 
 func NewOsFs(osDir string) *OsFS {
@@ -163,6 +158,7 @@ func (f *LazyOsFile) ReadAt(p []byte, off int64) (n int, err error) {
 
 func (f *LazyOsFile) Stat() (fs.FileInfo, error) {
 	f.m.Lock()
+	defer f.m.Unlock()
 	if f.info == nil {
 		if f.file == nil {
 			info, err := os.Stat(f.path)
@@ -178,7 +174,6 @@ func (f *LazyOsFile) Stat() (fs.FileInfo, error) {
 			f.info = info
 		}
 	}
-	f.m.Unlock()
 	return f.info, nil
 }
 
diff --git a/src/host/vfs/resolver.go b/src/host/vfs/resolver.go
index 5685d0b..a51957f 100644
--- a/src/host/vfs/resolver.go
+++ b/src/host/vfs/resolver.go
@@ -113,7 +113,29 @@ func (r *resolver) isNestedFs(f string) bool {
 			return true
 		}
 	}
-	return true
+	return false
+}
+
+func (r *resolver) nestedFs(fsPath string, file File) (Filesystem, error) {
+	for ext, nestFactory := range r.factories {
+		if !strings.HasSuffix(fsPath, ext) {
+			continue
+		}
+
+		if nestedFs, ok := r.fsmap[fsPath]; ok {
+			return nestedFs, nil
+		}
+
+		nestedFs, err := nestFactory(file)
+		if err != nil {
+			return nil, fmt.Errorf("error creating filesystem from file: %s with error: %w", fsPath, err)
+		}
+		r.fsmap[fsPath] = nestedFs
+
+		return nestedFs, nil
+
+	}
+	return nil, nil
 }
 
 // open requeue raw open, without resolver call
@@ -173,7 +195,7 @@ var ErrNotExist = fs.ErrNotExist
 
 func getFile[F File](m map[string]F, name string) (File, error) {
 	if name == Separator {
-		return &dir{}, nil
+		return NewDir(name), nil
 	}
 
 	f, ok := m[name]
@@ -183,7 +205,7 @@ func getFile[F File](m map[string]F, name string) (File, error) {
 
 	for p := range m {
 		if strings.HasPrefix(p, name) {
-			return &dir{}, nil
+			return NewDir(name), nil
 		}
 	}
 
diff --git a/src/host/vfs/torrent.go b/src/host/vfs/torrent.go
index 690768b..d793626 100644
--- a/src/host/vfs/torrent.go
+++ b/src/host/vfs/torrent.go
@@ -10,7 +10,7 @@ import (
 	"sync"
 	"time"
 
-	"git.kmsign.ru/royalcat/tstor/src/host/storage"
+	"git.kmsign.ru/royalcat/tstor/src/host/controller"
 	"git.kmsign.ru/royalcat/tstor/src/iio"
 	"github.com/anacrolix/missinggo/v2"
 	"github.com/anacrolix/torrent"
@@ -20,81 +20,115 @@ import (
 var _ Filesystem = &TorrentFs{}
 
 type TorrentFs struct {
-	mu  sync.Mutex
-	t   *torrent.Torrent
-	rep storage.ExlcudedFiles
+	mu sync.Mutex
+	c  *controller.Torrent
 
 	readTimeout int
 
-	//cache
-	filesCache map[string]*torrentFile
+	filesCache map[string]File
 
 	resolver *resolver
 }
 
-func NewTorrentFs(t *torrent.Torrent, rep storage.ExlcudedFiles, readTimeout int) *TorrentFs {
+func NewTorrentFs(c *controller.Torrent, readTimeout int) *TorrentFs {
 	return &TorrentFs{
-		t:           t,
-		rep:         rep,
+		c:           c,
 		readTimeout: readTimeout,
 		resolver:    newResolver(ArchiveFactories),
 	}
 }
 
-func (fs *TorrentFs) files() (map[string]*torrentFile, error) {
-	if fs.filesCache == nil {
-		fs.mu.Lock()
-		<-fs.t.GotInfo()
-		files := fs.t.Files()
+func (fs *TorrentFs) files() (map[string]File, error) {
+	fs.mu.Lock()
+	defer fs.mu.Unlock()
 
-		excludedFiles, err := fs.rep.ExcludedFiles(fs.t.InfoHash())
-		if err != nil {
-			return nil, err
+	if fs.filesCache != nil {
+		return fs.filesCache, nil
+	}
+
+	files, err := fs.c.Files()
+	if err != nil {
+		return nil, err
+	}
+
+	fs.filesCache = make(map[string]File)
+	for _, file := range files {
+		p := AbsPath(file.Path())
+
+		fs.filesCache[p] = &torrentFile{
+			name:    path.Base(p),
+			timeout: fs.readTimeout,
+			file:    file,
+		}
+	}
+
+	// TODO optional
+	if len(fs.filesCache) == 1 && fs.resolver.isNestedFs(fs.c.Name()) {
+		filepath := "/" + fs.c.Name()
+		if file, ok := fs.filesCache[filepath]; ok {
+			nestedFs, err := fs.resolver.nestedFs(filepath, file)
+			if err != nil {
+				return nil, err
+			}
+			if nestedFs == nil {
+				goto DEFAULT_DIR // FIXME
+			}
+			fs.filesCache, err = listFilesRecursive(nestedFs, "/")
+			if err != nil {
+				return nil, err
+			}
+
+			return fs.filesCache, nil
 		}
 
-		fs.filesCache = make(map[string]*torrentFile)
-		for _, file := range files {
-			p := file.Path()
+	}
 
-			if slices.Contains(excludedFiles, p) {
-				continue
-			}
-
-			if strings.Contains(p, "/.pad/") {
-				continue
-			}
-
-			p = AbsPath(file.Path())
-
-			fs.filesCache[p] = &torrentFile{
-				name:    path.Base(p),
-				timeout: fs.readTimeout,
-				file:    file,
-			}
+DEFAULT_DIR:
+	rootDir := "/" + fs.c.Name() + "/"
+	singleDir := true
+	for k, _ := range fs.filesCache {
+		if !strings.HasPrefix(k, rootDir) {
+			singleDir = false
 		}
-
-		rootDir := "/" + fs.t.Name() + "/"
-		singleDir := true
-		for k, _ := range fs.filesCache {
-			if !strings.HasPrefix(k, rootDir) {
-				singleDir = false
-			}
+	}
+	if singleDir {
+		for k, f := range fs.filesCache {
+			delete(fs.filesCache, k)
+			k, _ = strings.CutPrefix(k, rootDir)
+			k = AbsPath(k)
+			fs.filesCache[k] = f
 		}
-		if singleDir {
-			for k, f := range fs.filesCache {
-				delete(fs.filesCache, k)
-				k, _ = strings.CutPrefix(k, rootDir)
-				k = AbsPath(k)
-				fs.filesCache[k] = f
-			}
-		}
-
-		fs.mu.Unlock()
 	}
 
 	return fs.filesCache, nil
 }
 
+func listFilesRecursive(vfs Filesystem, start string) (map[string]File, error) {
+	out := make(map[string]File, 0)
+	entries, err := vfs.ReadDir(start)
+	if err != nil {
+		return nil, err
+	}
+	for _, entry := range entries {
+		filename := path.Join(start, entry.Name())
+		if entry.IsDir() {
+			rec, err := listFilesRecursive(vfs, filename)
+			if err != nil {
+				return nil, err
+			}
+			maps.Copy(out, rec)
+		} else {
+			file, err := vfs.Open(filename)
+			if err != nil {
+				return nil, err
+			}
+			out[filename] = file
+		}
+	}
+
+	return out, nil
+}
+
 func (fs *TorrentFs) rawOpen(path string) (File, error) {
 	files, err := fs.files()
 	if err != nil {
@@ -113,12 +147,7 @@ func (fs *TorrentFs) rawStat(filename string) (fs.FileInfo, error) {
 	if err != nil {
 		return nil, err
 	}
-	if file.IsDir() {
-		return newDirInfo(path.Base(filename)), nil
-	} else {
-		return newFileInfo(path.Base(filename), file.Size()), nil
-	}
-
+	return file.Stat()
 }
 
 // Stat implements Filesystem.
@@ -184,7 +213,12 @@ func (fs *TorrentFs) Unlink(name string) error {
 	file := files[name]
 	delete(fs.filesCache, name)
 
-	return fs.rep.ExcludeFile(file.file)
+	tfile, ok := file.(*torrentFile)
+	if !ok {
+		return ErrNotImplemented
+	}
+
+	return fs.c.ExcludeFile(tfile.file)
 }
 
 type reader interface {
diff --git a/src/host/vfs/utils.go b/src/host/vfs/utils.go
index e89977c..799ed6f 100644
--- a/src/host/vfs/utils.go
+++ b/src/host/vfs/utils.go
@@ -1,6 +1,9 @@
 package vfs
 
-import "strings"
+import (
+	"strings"
+	"sync"
+)
 
 func trimRelPath(p, t string) string {
 	return strings.Trim(strings.TrimPrefix(p, t), "/")
@@ -23,3 +26,28 @@ func AddTrailSlash(p string) string {
 	}
 	return p
 }
+
+// OnceValueWOErr returns a function that invokes f only once and returns the value
+// returned by f . The returned function may be called concurrently.
+//
+// If f panics, the returned function will panic with the same value on every call.
+func OnceValueWOErr[T any](f func() (T, error)) func() (T, error) {
+	var (
+		mu         sync.Mutex
+		isExecuted bool
+		r1         T
+		err        error
+	)
+
+	return func() (T, error) {
+		mu.Lock()
+		defer mu.Unlock()
+
+		if isExecuted && err == nil {
+			return r1, nil
+		}
+
+		r1, err = f()
+		return r1, err
+	}
+}
diff --git a/src/http/http.go b/src/http/http.go
index d10064a..495949a 100644
--- a/src/http/http.go
+++ b/src/http/http.go
@@ -2,19 +2,22 @@ package http
 
 import (
 	"fmt"
+	"log/slog"
 	"net/http"
 
 	"git.kmsign.ru/royalcat/tstor"
 	"git.kmsign.ru/royalcat/tstor/src/config"
+	"git.kmsign.ru/royalcat/tstor/src/delivery"
 	"git.kmsign.ru/royalcat/tstor/src/host/service"
 	"github.com/anacrolix/missinggo/v2/filecache"
 	"github.com/gin-contrib/pprof"
 	"github.com/gin-gonic/gin"
-	"github.com/rs/zerolog/log"
 	"github.com/shurcooL/httpfs/html/vfstemplate"
 )
 
 func New(fc *filecache.Cache, ss *service.Stats, s *service.Service, logPath string, cfg *config.Config) error {
+	log := slog.With()
+
 	gin.SetMode(gin.ReleaseMode)
 	r := gin.New()
 	r.Use(gin.Recovery())
@@ -37,6 +40,7 @@ func New(fc *filecache.Cache, ss *service.Stats, s *service.Service, logPath str
 	// r.GET("/routes", routesHandler(ss))
 	r.GET("/logs", logsHandler)
 	r.GET("/servers", serversFoldersHandler())
+	r.Any("/graphql", gin.WrapH(delivery.GraphQLHandler(s)))
 
 	api := r.Group("/api")
 	{
@@ -50,7 +54,7 @@ func New(fc *filecache.Cache, ss *service.Stats, s *service.Service, logPath str
 
 	}
 
-	log.Info().Str("host", fmt.Sprintf("%s:%d", cfg.WebUi.IP, cfg.WebUi.Port)).Msg("starting webserver")
+	log.Info("starting webserver", "host", fmt.Sprintf("%s:%d", cfg.WebUi.IP, cfg.WebUi.Port))
 
 	if err := r.Run(fmt.Sprintf("%s:%d", cfg.WebUi.IP, cfg.WebUi.Port)); err != nil {
 		return fmt.Errorf("error initializing server: %w", err)
@@ -60,7 +64,7 @@ func New(fc *filecache.Cache, ss *service.Stats, s *service.Service, logPath str
 }
 
 func Logger() gin.HandlerFunc {
-	l := log.Logger.With().Str("component", "http").Logger()
+	l := slog.With("component", "http")
 	return func(c *gin.Context) {
 		path := c.Request.URL.Path
 		raw := c.Request.URL.RawQuery
@@ -76,11 +80,11 @@ func Logger() gin.HandlerFunc {
 		s := c.Writer.Status()
 		switch {
 		case s >= 400 && s < 500:
-			l.Warn().Str("path", path).Int("status", s).Msg(msg)
+			l.Warn(msg, "path", path, "status", s)
 		case s >= 500:
-			l.Error().Str("path", path).Int("status", s).Msg(msg)
+			l.Error(msg, "path", path, "status", s)
 		default:
-			l.Debug().Str("path", path).Int("status", s).Msg(msg)
+			l.Debug(msg, "path", path, "status", s)
 		}
 	}
 }
diff --git a/src/log/badger.go b/src/log/badger.go
index 76d80ea..f547dde 100644
--- a/src/log/badger.go
+++ b/src/log/badger.go
@@ -1,30 +1,35 @@
 package log
 
 import (
+	"fmt"
+	"log/slog"
 	"strings"
 
 	"github.com/dgraph-io/badger/v4"
-	"github.com/rs/zerolog"
 )
 
 var _ badger.Logger = (*Badger)(nil)
 
 type Badger struct {
-	L zerolog.Logger
+	L *slog.Logger
+}
+
+func fmtBadgerLog(m string, f ...any) string {
+	return fmt.Sprintf(strings.ReplaceAll(m, "\n", ""), f...)
 }
 
 func (l *Badger) Errorf(m string, f ...interface{}) {
-	l.L.Error().Msgf(strings.ReplaceAll(m, "\n", ""), f...)
+	l.L.Error(fmtBadgerLog(m, f...))
 }
 
 func (l *Badger) Warningf(m string, f ...interface{}) {
-	l.L.Warn().Msgf(strings.ReplaceAll(m, "\n", ""), f...)
+	l.L.Warn(fmtBadgerLog(m, f...))
 }
 
 func (l *Badger) Infof(m string, f ...interface{}) {
-	l.L.Info().Msgf(strings.ReplaceAll(m, "\n", ""), f...)
+	l.L.Info(fmtBadgerLog(m, f...))
 }
 
 func (l *Badger) Debugf(m string, f ...interface{}) {
-	l.L.Debug().Msgf(strings.ReplaceAll(m, "\n", ""), f...)
+	l.L.Debug(fmtBadgerLog(m, f...))
 }
diff --git a/src/log/log.go b/src/log/log.go
index d7539ac..654de65 100644
--- a/src/log/log.go
+++ b/src/log/log.go
@@ -1,50 +1,41 @@
 package log
 
 import (
-	"io"
+	"log/slog"
 	"os"
-	"path/filepath"
+	"time"
 
 	"git.kmsign.ru/royalcat/tstor/src/config"
-	"github.com/mattn/go-colorable"
-	"github.com/rs/zerolog"
-	"github.com/rs/zerolog/log"
-	"gopkg.in/natefinch/lumberjack.v2"
+	"github.com/lmittmann/tint"
 )
 
 const FileName = "tstor.log"
 
 func Load(config *config.Log) {
-	var writers []io.Writer
-
-	// fix console colors on windows
-	cso := colorable.NewColorableStdout()
-
-	writers = append(writers, zerolog.ConsoleWriter{Out: cso})
-	writers = append(writers, newRollingFile(config))
-	mw := io.MultiWriter(writers...)
-
-	log.Logger = log.Output(mw)
-	zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
-
-	l := zerolog.InfoLevel
+	level := slog.LevelInfo
 	if config.Debug {
-		l = zerolog.DebugLevel
+		level = slog.LevelDebug
 	}
 
-	zerolog.SetGlobalLevel(l)
+	slog.SetDefault(slog.New(
+		tint.NewHandler(os.Stdout, &tint.Options{
+			Level:      level,
+			TimeFormat: time.Kitchen,
+			// NoColor:    !isatty.IsTerminal(os.Stdout.Fd()),
+		}),
+	))
 }
 
-func newRollingFile(config *config.Log) io.Writer {
-	if err := os.MkdirAll(config.Path, 0744); err != nil {
-		log.Error().Err(err).Str("path", config.Path).Msg("can't create log directory")
-		return nil
-	}
+// func newRollingFile(config *config.Log) io.Writer {
+// 	if err := os.MkdirAll(config.Path, 0744); err != nil {
+// 		log.Error().Err(err).Str("path", config.Path).Msg("can't create log directory")
+// 		return nil
+// 	}
 
-	return &lumberjack.Logger{
-		Filename:   filepath.Join(config.Path, FileName),
-		MaxBackups: config.MaxBackups, // files
-		MaxSize:    config.MaxSize,    // megabytes
-		MaxAge:     config.MaxAge,     // days
-	}
-}
+// 	return &lumberjack.Logger{
+// 		Filename:   filepath.Join(config.Path, FileName),
+// 		MaxBackups: config.MaxBackups, // files
+// 		MaxSize:    config.MaxSize,    // megabytes
+// 		MaxAge:     config.MaxAge,     // days
+// 	}
+// }
diff --git a/src/log/nfs.go b/src/log/nfs.go
index 422b6b9..cde0632 100644
--- a/src/log/nfs.go
+++ b/src/log/nfs.go
@@ -2,172 +2,179 @@ package log
 
 import (
 	"fmt"
+	"log"
+	"log/slog"
 
-	"github.com/rs/zerolog"
 	nfs "github.com/willscott/go-nfs"
 )
 
 var _ nfs.Logger = (*NFSLog)(nil)
 
 type NFSLog struct {
-	r zerolog.Logger
-	l zerolog.Logger
+	// r *slog.Logger
+	l *slog.Logger
 }
 
-func NewNFSLog(r zerolog.Logger) nfs.Logger {
+func NewNFSLog(r *slog.Logger) nfs.Logger {
 	return &NFSLog{
-		r: r,
-		l: r.Level(zerolog.DebugLevel),
+		// r: r,
+		// l: r.Level(zerolog.DebugLevel),
+		l: r,
 	}
 }
 
 // Debug implements nfs.Logger.
 func (l *NFSLog) Debug(args ...interface{}) {
-	l.l.Debug().Msg(fmt.Sprint(args...))
+	l.l.Debug(fmt.Sprint(args...))
 }
 
 // Debugf implements nfs.Logger.
 func (l *NFSLog) Debugf(format string, args ...interface{}) {
-	l.l.Debug().Msgf(format, args...)
+	l.l.Debug(fmt.Sprintf(format, args...))
 }
 
 // Error implements nfs.Logger.
 func (l *NFSLog) Error(args ...interface{}) {
-	l.l.Error().Msg(fmt.Sprint(args...))
+	l.l.Error(fmt.Sprint(args...))
 }
 
 // Errorf implements nfs.Logger.
 func (l *NFSLog) Errorf(format string, args ...interface{}) {
-	l.l.Error().Msgf(format, args...)
+	l.l.Error(fmt.Sprintf(format, args...))
 }
 
 // Fatal implements nfs.Logger.
 func (l *NFSLog) Fatal(args ...interface{}) {
-	l.l.Fatal().Msg(fmt.Sprint(args...))
+	l.l.Error(fmt.Sprint(args...))
+	log.Fatal(args...)
 }
 
 // Fatalf implements nfs.Logger.
 func (l *NFSLog) Fatalf(format string, args ...interface{}) {
-	l.l.Fatal().Msgf(format, args...)
+	l.l.Error(fmt.Sprintf(format, args...))
+	log.Fatalf(format, args...)
 }
 
 // Info implements nfs.Logger.
 func (l *NFSLog) Info(args ...interface{}) {
-	l.l.Info().Msg(fmt.Sprint(args...))
+	l.l.Info(fmt.Sprint(args...))
 }
 
 // Infof implements nfs.Logger.
 func (l *NFSLog) Infof(format string, args ...interface{}) {
-	l.l.Info().Msgf(format, args...)
+	l.l.Info(fmt.Sprintf(format, args...))
 }
 
 // Panic implements nfs.Logger.
 func (l *NFSLog) Panic(args ...interface{}) {
-	l.l.Panic().Msg(fmt.Sprint(args...))
+	l.l.Error(fmt.Sprint(args...))
+	panic(args)
 }
 
 // Panicf implements nfs.Logger.
 func (l *NFSLog) Panicf(format string, args ...interface{}) {
-	l.l.Panic().Msgf(format, args...)
+	l.l.Error(fmt.Sprintf(format, args...))
+	panic(args)
 }
 
 // Print implements nfs.Logger.
 func (l *NFSLog) Print(args ...interface{}) {
-	l.l.Print(args...)
+	l.l.Info(fmt.Sprint(args...))
 }
 
 // Printf implements nfs.Logger.
 func (l *NFSLog) Printf(format string, args ...interface{}) {
-	l.l.Printf(format, args...)
+	l.l.Info(fmt.Sprintf(format, args...))
 }
 
 // Trace implements nfs.Logger.
 func (l *NFSLog) Trace(args ...interface{}) {
-	l.l.Trace().Msg(fmt.Sprint(args...))
+	l.l.Debug(fmt.Sprint(args...))
 }
 
 // Tracef implements nfs.Logger.
 func (l *NFSLog) Tracef(format string, args ...interface{}) {
-	l.l.Trace().Msgf(format, args...)
+	l.l.Debug(fmt.Sprintf(format, args...))
 }
 
 // Warn implements nfs.Logger.
 func (l *NFSLog) Warn(args ...interface{}) {
-	l.l.Warn().Msg(fmt.Sprint(args...))
+	l.l.Warn(fmt.Sprint(args...))
 }
 
 // Warnf implements nfs.Logger.
 func (l *NFSLog) Warnf(format string, args ...interface{}) {
-	l.l.Warn().Msgf(format, args...)
+	l.l.Warn(fmt.Sprintf(format, args...))
 }
 
 // GetLevel implements nfs.Logger.
 func (l *NFSLog) GetLevel() nfs.LogLevel {
-	zl := l.l.GetLevel()
-	switch zl {
-	case zerolog.PanicLevel, zerolog.Disabled:
-		return nfs.PanicLevel
-	case zerolog.FatalLevel:
-		return nfs.FatalLevel
-	case zerolog.ErrorLevel:
-		return nfs.ErrorLevel
-	case zerolog.WarnLevel:
-		return nfs.WarnLevel
-	case zerolog.InfoLevel:
-		return nfs.InfoLevel
-	case zerolog.DebugLevel:
-		return nfs.DebugLevel
-	case zerolog.TraceLevel:
-		return nfs.TraceLevel
-	}
-	return nfs.DebugLevel
+	// zl := l.l.Handler()
+	// switch zl {
+	// case zerolog.PanicLevel, zerolog.Disabled:
+	// 	return nfs.PanicLevel
+	// case zerolog.FatalLevel:
+	// 	return nfs.FatalLevel
+	// case zerolog.ErrorLevel:
+	// 	return nfs.ErrorLevel
+	// case zerolog.WarnLevel:
+	// 	return nfs.WarnLevel
+	// case zerolog.InfoLevel:
+	// 	return nfs.InfoLevel
+	// case zerolog.DebugLevel:
+	// 	return nfs.DebugLevel
+	// case zerolog.TraceLevel:
+	// 	return nfs.TraceLevel
+	// }
+	return nfs.TraceLevel
 }
 
 // ParseLevel implements nfs.Logger.
 func (l *NFSLog) ParseLevel(level string) (nfs.LogLevel, error) {
-	switch level {
-	case "panic":
-		return nfs.PanicLevel, nil
-	case "fatal":
-		return nfs.FatalLevel, nil
-	case "error":
-		return nfs.ErrorLevel, nil
-	case "warn":
-		return nfs.WarnLevel, nil
-	case "info":
-		return nfs.InfoLevel, nil
-	case "debug":
-		return nfs.DebugLevel, nil
-	case "trace":
-		return nfs.TraceLevel, nil
-	}
-	var ll nfs.LogLevel
-	return ll, fmt.Errorf("invalid log level %q", level)
+	// switch level {
+	// case "panic":
+	// 	return nfs.PanicLevel, nil
+	// case "fatal":
+	// 	return nfs.FatalLevel, nil
+	// case "error":
+	// 	return nfs.ErrorLevel, nil
+	// case "warn":
+	// 	return nfs.WarnLevel, nil
+	// case "info":
+	// 	return nfs.InfoLevel, nil
+	// case "debug":
+	// 	return nfs.DebugLevel, nil
+	// case "trace":
+	// 	return nfs.TraceLevel, nil
+	// }
+	// var ll nfs.LogLevel
+	// return ll, fmt.Errorf("invalid log level %q", level)
+	return nfs.TraceLevel, fmt.Errorf("level change not supported")
 }
 
 // SetLevel implements nfs.Logger.
 func (l *NFSLog) SetLevel(level nfs.LogLevel) {
-	switch level {
-	case nfs.PanicLevel:
-		l.l = l.r.Level(zerolog.PanicLevel)
-		return
-	case nfs.FatalLevel:
-		l.l = l.r.Level(zerolog.FatalLevel)
-		return
-	case nfs.ErrorLevel:
-		l.l = l.r.Level(zerolog.ErrorLevel)
-		return
-	case nfs.WarnLevel:
-		l.l = l.r.Level(zerolog.WarnLevel)
-		return
-	case nfs.InfoLevel:
-		l.l = l.r.Level(zerolog.InfoLevel)
-		return
-	case nfs.DebugLevel:
-		l.l = l.r.Level(zerolog.DebugLevel)
-		return
-	case nfs.TraceLevel:
-		l.l = l.r.Level(zerolog.TraceLevel)
-		return
-	}
+	// switch level {
+	// case nfs.PanicLevel:
+	// 	l.l = l.r.Level(zerolog.PanicLevel)
+	// 	return
+	// case nfs.FatalLevel:
+	// 	l.l = l.r.Level(zerolog.FatalLevel)
+	// 	return
+	// case nfs.ErrorLevel:
+	// 	l.l = l.r.Level(zerolog.ErrorLevel)
+	// 	return
+	// case nfs.WarnLevel:
+	// 	l.l = l.r.Level(zerolog.WarnLevel)
+	// 	return
+	// case nfs.InfoLevel:
+	// 	l.l = l.r.Level(zerolog.InfoLevel)
+	// 	return
+	// case nfs.DebugLevel:
+	// 	l.l = l.r.Level(zerolog.DebugLevel)
+	// 	return
+	// case nfs.TraceLevel:
+	// 	l.l = l.r.Level(zerolog.TraceLevel)
+	// 	return
+	// }
 }
diff --git a/src/log/torrent.go b/src/log/torrent.go
index 4d28d8d..62c2c8d 100644
--- a/src/log/torrent.go
+++ b/src/log/torrent.go
@@ -1,32 +1,34 @@
 package log
 
 import (
+	"context"
+	"log/slog"
+
 	"github.com/anacrolix/log"
-	"github.com/rs/zerolog"
 )
 
 var _ log.Handler = &Torrent{}
 
 type Torrent struct {
-	L zerolog.Logger
+	L *slog.Logger
 }
 
 func (l *Torrent) Handle(r log.Record) {
-	e := l.L.Info()
+	lvl := slog.LevelInfo
 	switch r.Level {
 	case log.Debug:
-		e = l.L.Debug()
+		lvl = slog.LevelInfo
 	case log.Info:
-		e = l.L.Debug().Str("error-type", "info")
+		lvl = slog.LevelInfo
 	case log.Warning:
-		e = l.L.Warn()
+		lvl = slog.LevelWarn
 	case log.Error:
-		e = l.L.Warn().Str("error-type", "error")
+		lvl = slog.LevelError
 	case log.Critical:
-		e = l.L.Warn().Str("error-type", "critical")
+		lvl = slog.LevelError
 	}
 
 	// TODO set log values somehow
 
-	e.Msgf(r.Text())
+	l.L.Log(context.Background(), lvl, r.Msg.String())
 }
diff --git a/src/proto/gen.go b/src/proto/gen.go
deleted file mode 100644
index 55007dd..0000000
--- a/src/proto/gen.go
+++ /dev/null
@@ -1,3 +0,0 @@
-package proto
-
-//go:generate protoc --go_out=. --go_opt=paths=source_relative --go_opt=Mtstor.proto=git.kmsign.ru/royalcat/tstor/src/proto --go-grpc_out=. --go-grpc_opt=paths=source_relative --go-grpc_opt=Mtstor.proto=git.kmsign.ru/royalcat/tstor/src/proto  --proto_path=../../proto tstor.proto
diff --git a/tools.go b/tools.go
new file mode 100644
index 0000000..a33b059
--- /dev/null
+++ b/tools.go
@@ -0,0 +1,9 @@
+//go:build tools
+// +build tools
+
+//go:generate go run github.com/99designs/gqlgen
+package tstor
+
+import (
+	_ "github.com/99designs/gqlgen"
+)

From 35913e019007f75e4a84d4580cf3f3909f27203a Mon Sep 17 00:00:00 2001
From: royalcat <k.adamovich20@gmail.com>
Date: Fri, 23 Feb 2024 01:54:56 +0300
Subject: [PATCH 12/18] WIP

---
 go.mod                                        |   6 +-
 go.sum                                        |   8 +-
 src/delivery/graphql/model/filter.go          |   2 +-
 .../graphql/resolver/query.resolvers.go       |   6 +-
 .../graphql/resolver/torrent.resolvers.go     |  11 +-
 src/host/controller/torrent.go                |  19 +++-
 .../{storage_files.go => storage.go}          |  13 ++-
 src/host/service/service.go                   |  41 ++++++-
 src/host/store/client.go                      |  29 ++++-
 src/host/store/info.go                        |  21 +++-
 src/host/store/stats.go                       | 106 ++++++++++++++++++
 src/host/vfs/torrent.go                       |   4 +
 12 files changed, 236 insertions(+), 30 deletions(-)
 rename src/host/filestorage/{storage_files.go => storage.go} (93%)
 create mode 100644 src/host/store/stats.go

diff --git a/go.mod b/go.mod
index 8477be3..eb52aa8 100644
--- a/go.mod
+++ b/go.mod
@@ -5,12 +5,13 @@ go 1.21
 require (
 	github.com/99designs/gqlgen v0.17.43
 	github.com/anacrolix/dht/v2 v2.21.0
-	github.com/anacrolix/log v0.14.5
+	github.com/anacrolix/log v0.14.6-0.20231202035202-ed7a02cad0b4
 	github.com/anacrolix/missinggo/v2 v2.7.3
-	github.com/anacrolix/torrent v1.53.2
+	github.com/anacrolix/torrent v1.54.0
 	github.com/billziss-gh/cgofuse v1.5.0
 	github.com/bodgit/sevenzip v1.4.5
 	github.com/dgraph-io/badger/v4 v4.2.0
+	github.com/dgraph-io/ristretto v0.1.1
 	github.com/gin-contrib/pprof v1.4.0
 	github.com/gin-gonic/gin v1.9.1
 	github.com/go-git/go-billy/v5 v5.5.0
@@ -64,7 +65,6 @@ require (
 	github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/dgraph-io/badger v1.6.0 // indirect
-	github.com/dgraph-io/ristretto v0.1.1 // indirect
 	github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 // indirect
 	github.com/dustin/go-humanize v1.0.0 // indirect
 	github.com/edsrzf/mmap-go v1.1.0 // indirect
diff --git a/go.sum b/go.sum
index acc9f71..9475909 100644
--- a/go.sum
+++ b/go.sum
@@ -64,8 +64,8 @@ github.com/anacrolix/log v0.3.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgw
 github.com/anacrolix/log v0.6.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU=
 github.com/anacrolix/log v0.10.1-0.20220123034749-3920702c17f8/go.mod h1:GmnE2c0nvz8pOIPUSC9Rawgefy1sDXqposC2wgtBZE4=
 github.com/anacrolix/log v0.13.1/go.mod h1:D4+CvN8SnruK6zIFS/xPoRJmtvtnxs+CSfDQ+BFxZ68=
-github.com/anacrolix/log v0.14.5 h1:OkMjBquVSRb742LkecSGDGaGpNoSrw4syRIm0eRdmrg=
-github.com/anacrolix/log v0.14.5/go.mod h1:1OmJESOtxQGNMlUO5rcv96Vpp9mfMqXXbe2RdinFLdY=
+github.com/anacrolix/log v0.14.6-0.20231202035202-ed7a02cad0b4 h1:CdVK9IoqoqklXQQ4+L2aew64xsz14KdOD+rnKdTQajg=
+github.com/anacrolix/log v0.14.6-0.20231202035202-ed7a02cad0b4/go.mod h1:1OmJESOtxQGNMlUO5rcv96Vpp9mfMqXXbe2RdinFLdY=
 github.com/anacrolix/lsan v0.0.0-20211126052245-807000409a62 h1:P04VG6Td13FHMgS5ZBcJX23NPC/fiC4cp9bXwYujdYM=
 github.com/anacrolix/lsan v0.0.0-20211126052245-807000409a62/go.mod h1:66cFKPCO7Sl4vbFnAaSq7e4OXtdMhRSBagJGWgmpJbM=
 github.com/anacrolix/missinggo v0.0.0-20180725070939-60ef2fbf63df/go.mod h1:kwGiTUTZ0+p4vAz3VbAI5a30t2YbvemcmspjKwrAz5s=
@@ -96,8 +96,8 @@ github.com/anacrolix/sync v0.5.1/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DC
 github.com/anacrolix/tagflag v0.0.0-20180109131632-2146c8d41bf0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw=
 github.com/anacrolix/tagflag v1.0.0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw=
 github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8=
-github.com/anacrolix/torrent v1.53.2 h1:dW+ficSC8sJaGrUvZJizORPBLTP7XR8idl2oGlrUutQ=
-github.com/anacrolix/torrent v1.53.2/go.mod h1:d1NANCFAd9/nv9vmHnYUobLdyBSAoFYohojHjGmcAsw=
+github.com/anacrolix/torrent v1.54.0 h1:sl+2J1pHjJWq6+5G861+Yc74k2XTc/m8ijaMQR/8+2k=
+github.com/anacrolix/torrent v1.54.0/go.mod h1:is8GNob5qDeZ5Kq+pKPiE2xqYUi1ms7IgSB+CftZETk=
 github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96 h1:QAVZ3pN/J4/UziniAhJR2OZ9Ox5kOY2053tBbbqUPYA=
 github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96/go.mod h1:Wa6n8cYIdaG35x15aH3Zy6d03f7P728QfdcDeD/IEOs=
 github.com/anacrolix/utp v0.1.0 h1:FOpQOmIwYsnENnz7tAGohA+r6iXpRjrq8ssKSre2Cp4=
diff --git a/src/delivery/graphql/model/filter.go b/src/delivery/graphql/model/filter.go
index 911216b..0c96e46 100644
--- a/src/delivery/graphql/model/filter.go
+++ b/src/delivery/graphql/model/filter.go
@@ -2,7 +2,7 @@ package model
 
 import "slices"
 
-func (f *IntFilter) IsValid(v int64) bool {
+func (f *IntFilter) Include(v int64) bool {
 	if f.Eq != nil {
 		return v == *f.Eq
 	} else if f.Gt != nil {
diff --git a/src/delivery/graphql/resolver/query.resolvers.go b/src/delivery/graphql/resolver/query.resolvers.go
index f014380..9aecef7 100644
--- a/src/delivery/graphql/resolver/query.resolvers.go
+++ b/src/delivery/graphql/resolver/query.resolvers.go
@@ -23,17 +23,17 @@ func (r *queryResolver) Torrents(ctx context.Context, filter *model.TorrentsFilt
 	if filter != nil {
 		if filter.BytesCompleted != nil {
 			filterFuncs = append(filterFuncs, func(torrent *model.Torrent) bool {
-				return filter.BytesCompleted.IsValid(torrent.BytesCompleted)
+				return filter.BytesCompleted.Include(torrent.BytesCompleted)
 			})
 		}
 		if filter.BytesMissing != nil {
 			filterFuncs = append(filterFuncs, func(torrent *model.Torrent) bool {
-				return filter.BytesMissing.IsValid(torrent.BytesMissing)
+				return filter.BytesMissing.Include(torrent.BytesMissing)
 			})
 		}
 		if filter.PeersCount != nil {
 			filterFuncs = append(filterFuncs, func(torrent *model.Torrent) bool {
-				return filter.PeersCount.IsValid(
+				return filter.PeersCount.Include(
 					int64(len(torrent.T.Torrent().PeerConns())),
 				)
 			})
diff --git a/src/delivery/graphql/resolver/torrent.resolvers.go b/src/delivery/graphql/resolver/torrent.resolvers.go
index dfc9fd0..778fbf6 100644
--- a/src/delivery/graphql/resolver/torrent.resolvers.go
+++ b/src/delivery/graphql/resolver/torrent.resolvers.go
@@ -55,13 +55,16 @@ func (r *torrentResolver) ExcludedFiles(ctx context.Context, obj *model.Torrent)
 func (r *torrentResolver) Peers(ctx context.Context, obj *model.Torrent) ([]*model.TorrentPeer, error) {
 	peers := []*model.TorrentPeer{}
 	for _, peer := range obj.T.Torrent().PeerConns() {
+		clientName, _ := peer.PeerClientName.Load().(string)
+
 		peers = append(peers, &model.TorrentPeer{
 			IP:           peer.RemoteAddr.String(),
 			DownloadRate: peer.DownloadRate(),
-			Discovery:    model.MapPeerSource(peer.Discovery),
-			Port:         int64(peer.PeerListenPort),
-			ClientName:   peer.PeerClientName.Load().(string),
-			F:            peer,
+
+			Discovery:  model.MapPeerSource(peer.Discovery),
+			Port:       int64(peer.PeerListenPort),
+			ClientName: clientName,
+			F:          peer,
 		})
 	}
 	return peers, nil
diff --git a/src/host/controller/torrent.go b/src/host/controller/torrent.go
index 3a04b41..911a4d3 100644
--- a/src/host/controller/torrent.go
+++ b/src/host/controller/torrent.go
@@ -28,7 +28,11 @@ func (s *Torrent) Torrent() *torrent.Torrent {
 
 func (s *Torrent) Name() string {
 	<-s.t.GotInfo()
-	return s.t.Name()
+	if name := s.t.Name(); name != "" {
+		return name
+	}
+
+	return s.InfoHash()
 }
 
 func (s *Torrent) InfoHash() string {
@@ -68,9 +72,22 @@ func (s *Torrent) Files() ([]*torrent.File, error) {
 		return true
 	})
 
+	for _, tf := range files {
+		s.isFileComplete(tf.BeginPieceIndex(), tf.EndPieceIndex())
+	}
+
 	return files, nil
 }
 
+func (s *Torrent) isFileComplete(startIndex int, endIndex int) bool {
+	for i := startIndex; i < endIndex; i++ {
+		if !s.t.Piece(i).State().Complete {
+			return false
+		}
+	}
+	return true
+}
+
 func (s *Torrent) ExcludedFiles() ([]*torrent.File, error) {
 	excludedFiles, err := s.rep.ExcludedFiles(s.t.InfoHash())
 	if err != nil {
diff --git a/src/host/filestorage/storage_files.go b/src/host/filestorage/storage.go
similarity index 93%
rename from src/host/filestorage/storage_files.go
rename to src/host/filestorage/storage.go
index 8b3fa8c..cc933e2 100644
--- a/src/host/filestorage/storage_files.go
+++ b/src/host/filestorage/storage.go
@@ -47,7 +47,12 @@ func (me *FileStorage) Close() error {
 }
 
 func torrentDir(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string {
-	return filepath.Join(baseDir, info.Name)
+	dirName := info.Name
+	if dirName == "" {
+		dirName = infoHash.HexString()
+	}
+
+	return filepath.Join(baseDir, dirName)
 }
 
 func filePath(opts storage.FilePathMakerOpts) string {
@@ -120,6 +125,12 @@ func (fs *FileStorage) CleanupDirs(ctx context.Context, expected []*controller.T
 	return len(toDelete), nil
 }
 
+// func (fs *FileStorage) IsCompatable(ctx context.Context, addition *controller.Torrent, dryRun bool) (bool, error) {
+// 	log := fs.log.With("function", "IsCompatable", "addition", addition.Name())
+
+// 	ifp
+// }
+
 func (fs *FileStorage) CleanupFiles(ctx context.Context, expected []*controller.Torrent, dryRun bool) (int, error) {
 	log := fs.log.With("function", "CleanupFiles", "expectedTorrents", len(expected), "dryRun", dryRun)
 
diff --git a/src/host/service/service.go b/src/host/service/service.go
index d99cb1a..718e30e 100644
--- a/src/host/service/service.go
+++ b/src/host/service/service.go
@@ -6,6 +6,7 @@ import (
 	"log/slog"
 	"os"
 	"path/filepath"
+	"slices"
 	"strings"
 	"time"
 
@@ -102,8 +103,14 @@ func (s *Service) AddTorrent(ctx context.Context, f vfs.File) (*torrent.Torrent,
 			infoBytes = nil
 		} else {
 			for _, t := range s.c.Torrents() {
-				if t.Name() == info.BestName() {
-					return nil, fmt.Errorf("torrent with name '%s' already exists", t.Name())
+				if t.Name() == info.BestName() && t.InfoHash() != spec.InfoHash {
+					<-t.GotInfo()
+					if !isTorrentCompatable(*t.Info(), info) {
+						return nil, fmt.Errorf(
+							"torrent with name '%s' not compatable existing infohash: %s, new: %s",
+							t.Name(), t.InfoHash().HexString(), spec.InfoHash.HexString(),
+						)
+					}
 				}
 			}
 		}
@@ -115,6 +122,8 @@ func (s *Service) AddTorrent(ctx context.Context, f vfs.File) (*torrent.Torrent,
 			ChunkSize: spec.ChunkSize,
 		})
 		t.AllowDataDownload()
+		t.AllowDataUpload()
+		t.DownloadAll()
 
 		select {
 		case <-ctx.Done():
@@ -134,6 +143,34 @@ func (s *Service) AddTorrent(ctx context.Context, f vfs.File) (*torrent.Torrent,
 	return t, nil
 }
 
+func isTorrentCompatable(existingInfo, newInfo metainfo.Info) bool {
+	existingFiles := slices.Clone(existingInfo.Files)
+	newFiles := slices.Clone(newInfo.Files)
+
+	pathCmp := func(a, b metainfo.FileInfo) int {
+		return slices.Compare(a.BestPath(), b.BestPath())
+	}
+	slices.SortStableFunc(existingFiles, pathCmp)
+	slices.SortStableFunc(newFiles, pathCmp)
+
+	// torrents basically equals
+	if slices.EqualFunc(existingFiles, newFiles, func(fi1, fi2 metainfo.FileInfo) bool {
+		return fi1.Length == fi2.Length && slices.Equal(fi1.BestPath(), fi1.BestPath())
+	}) {
+		return true
+	}
+
+	if len(newFiles) > len(existingFiles) {
+		all := append(existingFiles, newFiles...)
+		slices.SortStableFunc(all, pathCmp)
+		slices.CompactFunc(all, func(fi1, fi2 metainfo.FileInfo) bool {
+			return slices.Equal(fi1.BestPath(), fi2.BestPath()) && fi1.Length == fi2.Length
+		})
+	}
+
+	return false
+}
+
 func isValidInfoHashBytes(d []byte) bool {
 	var info metainfo.Info
 	err := bencode.Unmarshal(d, &info)
diff --git a/src/host/store/client.go b/src/host/store/client.go
index 8275424..904251d 100644
--- a/src/host/store/client.go
+++ b/src/host/store/client.go
@@ -20,9 +20,11 @@ func NewClient(st storage.ClientImpl, fis bep44.Store, cfg *config.TorrentClient
 	torrentCfg := torrent.NewDefaultClientConfig()
 	torrentCfg.PeerID = string(id[:])
 	torrentCfg.DefaultStorage = st
-	// torrentCfg.AlwaysWantConns = true
-	// torrentCfg.DisableAggressiveUpload = true
-	// torrentCfg.Seed = true
+	torrentCfg.AlwaysWantConns = true
+	torrentCfg.AcceptPeerConnections = true
+	torrentCfg.DisableAggressiveUpload = false
+
+	torrentCfg.Seed = true
 	// torrentCfg.DownloadRateLimiter = rate.NewLimiter(rate.Inf, 0)
 	// torrentCfg
 
@@ -30,13 +32,28 @@ func NewClient(st storage.ClientImpl, fis bep44.Store, cfg *config.TorrentClient
 	tl.SetHandlers(&dlog.Torrent{L: l})
 	torrentCfg.Logger = tl
 	torrentCfg.Callbacks.NewPeer = append(torrentCfg.Callbacks.NewPeer, func(p *torrent.Peer) {
-		l.Debug("new peer", "ip", p.RemoteAddr.String())
+		l := l.With("ip", p.RemoteAddr.String())
+		if p.Torrent() != nil {
+			l = l.With("torrent", p.Torrent().Name())
+		}
+
+		l.Debug("new peer")
+
 	})
 
-	torrentCfg.Callbacks.NewPeer = append(torrentCfg.Callbacks.PeerClosed, func(p *torrent.Peer) {
-		l.Debug("peer closed", "ip", p.RemoteAddr.String())
+	torrentCfg.Callbacks.PeerClosed = append(torrentCfg.Callbacks.PeerClosed, func(p *torrent.Peer) {
+		l := l.With("ip", p.RemoteAddr.String())
+		if p.Torrent() != nil {
+			l = l.With("torrent", p.Torrent().Name())
+		}
+
+		l.Debug("peer closed")
 	})
 
+	// torrentCfg.Callbacks.PeerConnClosed = append(torrentCfg.Callbacks.PeerConnClosed, func(c *torrent.PeerConn) {
+	// 	l.Debug("peer closed", "ip", c.RemoteAddr.String())
+	// })
+
 	// torrentCfg.ConfigureAnacrolixDhtServer = func(cfg *dht.ServerConfig) {
 	// 	cfg.Store = fis
 	// 	cfg.Exp = 2 * time.Hour
diff --git a/src/host/store/info.go b/src/host/store/info.go
index 44f5880..205a60c 100644
--- a/src/host/store/info.go
+++ b/src/host/store/info.go
@@ -56,16 +56,27 @@ func (k *InfoBytes) Get(ih infohash.T) (*metainfo.MetaInfo, error) {
 	return metainfo.Load(bytes.NewReader(data))
 }
 
-func (me *InfoBytes) SetBytes(ih infohash.T, bytes []byte) error {
+func (me *InfoBytes) SetBytes(ih infohash.T, data []byte) error {
 	return me.db.Update(func(txn *badger.Txn) error {
-		return txn.Set(ih.Bytes(), bytes)
+		item, err := txn.Get(ih.Bytes())
+		if err != nil {
+			if err == badger.ErrKeyNotFound {
+				return txn.Set(ih.Bytes(), data)
+			}
+			return err
+		}
+
+		return item.Value(func(val []byte) error {
+			if !bytes.Equal(val, data) {
+				return txn.Set(ih.Bytes(), data)
+			}
+			return nil
+		})
 	})
 }
 
 func (me *InfoBytes) Set(ih infohash.T, info metainfo.MetaInfo) error {
-	return me.db.Update(func(txn *badger.Txn) error {
-		return txn.Set(ih.Bytes(), info.InfoBytes)
-	})
+	return me.SetBytes(ih, info.InfoBytes)
 }
 
 func (k *InfoBytes) Delete(ih infohash.T) error {
diff --git a/src/host/store/stats.go b/src/host/store/stats.go
new file mode 100644
index 0000000..0986566
--- /dev/null
+++ b/src/host/store/stats.go
@@ -0,0 +1,106 @@
+package store
+
+import (
+	"context"
+	"encoding/json"
+	"path"
+	"time"
+
+	"github.com/anacrolix/torrent/types/infohash"
+	"github.com/dgraph-io/badger/v4"
+	"github.com/dgraph-io/ristretto/z"
+)
+
+func NewStatsHistory(metaDir string, lifetime time.Duration) (*StatsHistory, error) {
+	db, err := badger.OpenManaged(
+		badger.
+			DefaultOptions(path.Join(metaDir, "stats-history")).
+			WithNumVersionsToKeep(int(^uint(0) >> 1)), // Infinity
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	go func() {
+		for n := range time.NewTimer(lifetime / 2).C {
+			db.SetDiscardTs(uint64(n.Add(-lifetime).Unix()))
+		}
+	}()
+	r := &StatsHistory{
+		db: db,
+	}
+
+	return r, nil
+}
+
+type StatsHistory struct {
+	db *badger.DB
+}
+
+type TorrentStat struct {
+	Name            string        `json:"name"`
+	Hash            string        `json:"hash"`
+	DownloadedBytes int64         `json:"downloadedBytes"`
+	UploadedBytes   int64         `json:"uploadedBytes"`
+	Peers           int           `json:"peers"`
+	Seeders         int           `json:"seeders"`
+	PieceChunks     []*PieceChunk `json:"pieceChunks"`
+	TotalPieces     int           `json:"totalPieces"`
+	PieceSize       int64         `json:"pieceSize"`
+}
+
+type PieceChunk struct {
+	Status    PieceStatus `json:"status"`
+	NumPieces int         `json:"numPieces"`
+}
+
+type PieceStatus string
+
+const (
+	Checking PieceStatus = "H"
+	Partial  PieceStatus = "P"
+	Complete PieceStatus = "C"
+	Waiting  PieceStatus = "W"
+	Error    PieceStatus = "?"
+)
+
+func (r *StatsHistory) AddStat(ih infohash.T, stat TorrentStat) error {
+	data, err := json.Marshal(stat)
+	if err != nil {
+		return err
+	}
+
+	return r.db.Update(func(txn *badger.Txn) error {
+		return txn.Set(ih.Bytes(), data)
+	})
+}
+
+func (r *StatsHistory) ReadStatsHistory(ctx context.Context, since time.Time) (GlobalTorrentStats, error) {
+	var stats GlobalTorrentStats
+	stream := r.db.NewStream()
+	stream.SinceTs = uint64(since.Unix())
+
+	var tstat TorrentStat
+	stream.Send = func(buf *z.Buffer) error {
+		err := json.Unmarshal(buf.Bytes(), &tstat)
+		if err != nil {
+			return err
+		}
+
+		stats.DownloadedBytes += tstat.DownloadedBytes
+		stats.UploadedBytes += tstat.UploadedBytes
+
+		return nil
+	}
+
+	err := stream.Orchestrate(ctx)
+	if err != nil {
+		return stats, err
+	}
+	return stats, nil
+}
+
+type GlobalTorrentStats struct {
+	DownloadedBytes int64 `json:"downloadedBytes"`
+	UploadedBytes   int64 `json:"uploadedBytes"`
+}
diff --git a/src/host/vfs/torrent.go b/src/host/vfs/torrent.go
index d793626..6d3069e 100644
--- a/src/host/vfs/torrent.go
+++ b/src/host/vfs/torrent.go
@@ -53,6 +53,10 @@ func (fs *TorrentFs) files() (map[string]File, error) {
 
 	fs.filesCache = make(map[string]File)
 	for _, file := range files {
+		if file.BytesCompleted() == 0 {
+			continue
+		}
+
 		p := AbsPath(file.Path())
 
 		fs.filesCache[p] = &torrentFile{

From 6a1e338af4cbbf7d8473681a92facfe9fac4f538 Mon Sep 17 00:00:00 2001
From: royalcat <k.adamovich20@gmail.com>
Date: Mon, 18 Mar 2024 00:00:34 +0300
Subject: [PATCH 13/18] update

---
 .gitignore                                    |   3 +-
 .gqlgen.yml                                   |   4 +
 .vscode/launch.json                           |   7 +
 Makefile                                      |  12 +-
 .../main.go                                   |   8 +-
 cmd/tstor/main.go                             |   8 +-
 go.mod                                        |  39 +-
 go.sum                                        | 109 +--
 graphql/mutation.graphql                      |  10 +
 graphql/schema.graphql                        |   2 +-
 graphql/subscription.graphql                  |  16 +
 pkg/uuid/uuid.go                              | 102 +++
 src/delivery/graphql/generated.go             | 814 +++++++++++++++++-
 src/delivery/graphql/model/mappers.go         |  15 +-
 src/delivery/graphql/model/models_gen.go      |  27 +
 .../graphql/resolver/mutation.resolvers.go    |  22 +
 .../graphql/resolver/query.resolvers.go       |   8 +-
 .../resolver/subscription.resolvers.go        |  54 ++
 .../graphql/resolver/torrent.resolvers.go     |  24 +-
 src/export/nfs/cache.go                       | 197 +++++
 src/export/nfs/handler.go                     |   2 +-
 src/host/controller/torrent.go                |  78 +-
 src/host/datastorage/piece_storage.go         | 172 ++++
 .../{filestorage => datastorage}/setup.go     |   4 +-
 .../{filestorage => datastorage}/storage.go   |   8 +-
 src/host/service/queue.go                     | 130 +++
 src/host/service/service.go                   |  38 +-
 src/host/store/excluded-files.go              |  94 --
 src/host/store/file-mappings.go               |  57 ++
 src/host/store/{store.go => fileitem.go}      |   0
 src/host/vfs/fs_test.go                       |   4 +-
 src/host/vfs/torrent.go                       |  28 +-
 src/host/vfs/virtdir/vds.go                   |  21 +
 src/log/nfs.go                                | 138 +--
 34 files changed, 1900 insertions(+), 355 deletions(-)
 rename cmd/{generate-graphlq => generate-graphql}/main.go (88%)
 create mode 100644 graphql/subscription.graphql
 create mode 100644 pkg/uuid/uuid.go
 create mode 100644 src/delivery/graphql/resolver/subscription.resolvers.go
 create mode 100644 src/export/nfs/cache.go
 create mode 100644 src/host/datastorage/piece_storage.go
 rename src/host/{filestorage => datastorage}/setup.go (93%)
 rename src/host/{filestorage => datastorage}/storage.go (94%)
 create mode 100644 src/host/service/queue.go
 delete mode 100644 src/host/store/excluded-files.go
 create mode 100644 src/host/store/file-mappings.go
 rename src/host/store/{store.go => fileitem.go} (100%)
 create mode 100644 src/host/vfs/virtdir/vds.go

diff --git a/.gitignore b/.gitignore
index fcc7b9b..5bbfe5f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,5 @@ httpfs_vfsdata.go
 bin/
 coverage.out
 bin
-build
\ No newline at end of file
+build
+deploy-debug.sh
\ No newline at end of file
diff --git a/.gqlgen.yml b/.gqlgen.yml
index 9f3f595..7363cf7 100644
--- a/.gqlgen.yml
+++ b/.gqlgen.yml
@@ -42,3 +42,7 @@ models:
     extraFields:
       F:
         type: "*github.com/anacrolix/torrent.PeerConn"
+  # TorrentProgress:
+  #   fields:
+  #     torrent:
+  #       resolver: true
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 4b9ec0e..bc0b8c4 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -4,6 +4,13 @@
     // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
     "version": "0.2.0",
     "configurations": [
+        {
+            "name": "Launch file",
+            "type": "go",
+            "request": "launch",
+            "mode": "debug",
+            "program": "${file}"
+        },
         {
             "name": "Launch Package",
             "type": "go",
diff --git a/Makefile b/Makefile
index beef8f0..028fa69 100644
--- a/Makefile
+++ b/Makefile
@@ -34,11 +34,7 @@ go-generate:
 	@echo "  >  Generating code files..."
 	go generate ./...
 
-.PHONY: help
-all: help
-help: Makefile
-	@echo
-	@echo " Choose a command run in "$(PROJECTNAME)":"
-	@echo
-	@sed -n 's/^##//p' $< | column -t -s ':' |  sed -e 's/^/ /'
-	@echo
+generate-graphql: src/delivery/graph/generated.go
+
+src/delivery/graph/generated.go: .gqlgen.yml graphql/* graphql/types/* cmd/generate-graphql/*
+	go run cmd/generate-graphql/main.go
diff --git a/cmd/generate-graphlq/main.go b/cmd/generate-graphql/main.go
similarity index 88%
rename from cmd/generate-graphlq/main.go
rename to cmd/generate-graphql/main.go
index 97ef427..ea2d6aa 100644
--- a/cmd/generate-graphlq/main.go
+++ b/cmd/generate-graphql/main.go
@@ -10,14 +10,14 @@ import (
 	"github.com/99designs/gqlgen/codegen/config"
 )
 
-type plugin_ struct {
+type fieldDirectiveFix struct {
 }
 
-func (plugin_) Name() string {
+func (fieldDirectiveFix) Name() string {
 	return "Fix Directive hook called with wrong object"
 }
 
-func (plugin_) GenerateCode(cfg *codegen.Data) error {
+func (fieldDirectiveFix) GenerateCode(cfg *codegen.Data) error {
 	for _, input := range cfg.Inputs {
 		for _, field := range input.Fields {
 			if field.GoFieldType == codegen.GoFieldVariable {
@@ -56,7 +56,7 @@ func main() {
 	}
 
 	err = api.Generate(cfg,
-		api.AddPlugin(&plugin_{}),
+		api.AddPlugin(&fieldDirectiveFix{}),
 	)
 	if err != nil {
 		fmt.Fprintln(os.Stderr, err.Error())
diff --git a/cmd/tstor/main.go b/cmd/tstor/main.go
index fe63a17..3b4dce0 100644
--- a/cmd/tstor/main.go
+++ b/cmd/tstor/main.go
@@ -15,7 +15,7 @@ import (
 
 	"git.kmsign.ru/royalcat/tstor/src/config"
 	"git.kmsign.ru/royalcat/tstor/src/host"
-	"git.kmsign.ru/royalcat/tstor/src/host/filestorage"
+	"git.kmsign.ru/royalcat/tstor/src/host/datastorage"
 	"git.kmsign.ru/royalcat/tstor/src/host/service"
 	"git.kmsign.ru/royalcat/tstor/src/host/store"
 	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
@@ -90,13 +90,13 @@ func run(configPath string) error {
 		return fmt.Errorf("error creating node ID: %w", err)
 	}
 
-	st, _, err := filestorage.Setup(conf.TorrentClient)
+	st, _, err := datastorage.Setup(conf.TorrentClient)
 	if err != nil {
 		return err
 	}
 	defer st.Close()
 
-	excludedFilesStore, err := store.NewExcludedFiles(conf.TorrentClient.MetadataFolder, st)
+	excludedFilesStore, err := store.NewFileMappings(conf.TorrentClient.MetadataFolder, st)
 	if err != nil {
 		return err
 	}
@@ -238,5 +238,5 @@ func run(configPath string) error {
 	signal.Notify(sigChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
 	<-sigChan
 
-	return nil
+	return ts.Close()
 }
diff --git a/go.mod b/go.mod
index eb52aa8..047396f 100644
--- a/go.mod
+++ b/go.mod
@@ -1,13 +1,14 @@
 module git.kmsign.ru/royalcat/tstor
 
-go 1.21
+go 1.22.1
 
 require (
 	github.com/99designs/gqlgen v0.17.43
-	github.com/anacrolix/dht/v2 v2.21.0
+	github.com/RoaringBitmap/roaring v1.2.3
+	github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444
 	github.com/anacrolix/log v0.14.6-0.20231202035202-ed7a02cad0b4
 	github.com/anacrolix/missinggo/v2 v2.7.3
-	github.com/anacrolix/torrent v1.54.0
+	github.com/anacrolix/torrent v1.55.0
 	github.com/billziss-gh/cgofuse v1.5.0
 	github.com/bodgit/sevenzip v1.4.5
 	github.com/dgraph-io/badger/v4 v4.2.0
@@ -15,6 +16,10 @@ require (
 	github.com/gin-contrib/pprof v1.4.0
 	github.com/gin-gonic/gin v1.9.1
 	github.com/go-git/go-billy/v5 v5.5.0
+	github.com/gofrs/uuid/v5 v5.0.0
+	github.com/google/uuid v1.5.0
+	github.com/hashicorp/go-multierror v1.1.1
+	github.com/hashicorp/golang-lru/v2 v2.0.7
 	github.com/knadh/koanf/parsers/yaml v0.1.0
 	github.com/knadh/koanf/providers/env v0.1.0
 	github.com/knadh/koanf/providers/file v0.1.0
@@ -22,27 +27,24 @@ require (
 	github.com/knadh/koanf/v2 v2.0.1
 	github.com/lmittmann/tint v1.0.4
 	github.com/nwaples/rardecode/v2 v2.0.0-beta.2
-	github.com/philippgille/gokv v0.6.0
-	github.com/philippgille/gokv/badgerdb v0.6.0
-	github.com/philippgille/gokv/encoding v0.6.0
+	github.com/royalcat/kv v0.0.0-20240316134516-1bb692abce73
 	github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c
 	github.com/stretchr/testify v1.8.4
 	github.com/urfave/cli/v2 v2.27.0
 	github.com/vektah/gqlparser/v2 v2.5.11
 	github.com/willscott/go-nfs v0.0.2
+	go.uber.org/multierr v1.11.0
 	golang.org/x/exp v0.0.0-20231226003508-02704c960a9b
 	golang.org/x/net v0.19.0
 )
 
 require (
-	github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
-	github.com/RoaringBitmap/roaring v1.2.3 // indirect
 	github.com/agnivade/levenshtein v1.1.1 // indirect
 	github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect
 	github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
 	github.com/anacrolix/chansync v0.3.0 // indirect
 	github.com/anacrolix/envpprof v1.3.0 // indirect
-	github.com/anacrolix/generics v0.0.0-20230816105729-c755655aee45 // indirect
+	github.com/anacrolix/generics v0.0.0-20230911070922-5dd7545c6b13 // indirect
 	github.com/anacrolix/go-libutp v1.3.1 // indirect
 	github.com/anacrolix/missinggo v1.3.0 // indirect
 	github.com/anacrolix/missinggo/perf v1.0.0 // indirect
@@ -60,12 +62,11 @@ require (
 	github.com/bodgit/windows v1.0.1 // indirect
 	github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
 	github.com/bytedance/sonic v1.9.1 // indirect
+	github.com/cespare/xxhash v1.1.0 // indirect
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 	github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
 	github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/dgraph-io/badger v1.6.0 // indirect
-	github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 // indirect
 	github.com/dustin/go-humanize v1.0.0 // indirect
 	github.com/edsrzf/mmap-go v1.1.0 // indirect
 	github.com/fatih/structs v1.1.0 // indirect
@@ -74,24 +75,21 @@ require (
 	github.com/gin-contrib/sse v0.1.0 // indirect
 	github.com/go-llsqlite/adapter v0.0.0-20230927005056-7f5ce7f0c916 // indirect
 	github.com/go-llsqlite/crawshaw v0.4.0 // indirect
-	github.com/go-logr/logr v1.2.3 // indirect
+	github.com/go-logr/logr v1.2.4 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-playground/locales v0.14.1 // indirect
 	github.com/go-playground/universal-translator v0.18.1 // indirect
 	github.com/go-playground/validator/v10 v10.14.0 // indirect
 	github.com/goccy/go-json v0.10.2 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
-	github.com/golang/glog v1.0.0 // indirect
+	github.com/golang/glog v1.1.0 // indirect
 	github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
-	github.com/golang/protobuf v1.5.2 // indirect
-	github.com/golang/snappy v0.0.3 // indirect
+	github.com/golang/protobuf v1.5.3 // indirect
+	github.com/golang/snappy v0.0.4 // indirect
 	github.com/google/btree v1.1.2 // indirect
-	github.com/google/flatbuffers v1.12.1 // indirect
-	github.com/google/uuid v1.5.0 // indirect
+	github.com/google/flatbuffers v2.0.8+incompatible // indirect
 	github.com/gorilla/websocket v1.5.0 // indirect
 	github.com/hashicorp/errwrap v1.0.0 // indirect
-	github.com/hashicorp/go-multierror v1.1.1 // indirect
-	github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
 	github.com/huandu/xstrings v1.3.2 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/klauspost/compress v1.17.4 // indirect
@@ -106,7 +104,6 @@ require (
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/mschoch/smat v0.2.0 // indirect
 	github.com/pelletier/go-toml/v2 v2.0.8 // indirect
-	github.com/philippgille/gokv/util v0.0.0-20191011213304-eb77f15b9c61 // indirect
 	github.com/pierrec/lz4/v4 v4.1.19 // indirect
 	github.com/pion/datachannel v1.5.2 // indirect
 	github.com/pion/dtls/v2 v2.2.4 // indirect
@@ -141,7 +138,7 @@ require (
 	github.com/willscott/go-nfs-client v0.0.0-20240104095149-b44639837b00 // indirect
 	github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
 	go.etcd.io/bbolt v1.3.6 // indirect
-	go.opencensus.io v0.22.5 // indirect
+	go.opencensus.io v0.24.0 // indirect
 	go.opentelemetry.io/otel v1.8.0 // indirect
 	go.opentelemetry.io/otel/trace v1.8.0 // indirect
 	go4.org v0.0.0-20200411211856-f5505b9728dd // indirect
diff --git a/go.sum b/go.sum
index 9475909..29df124 100644
--- a/go.sum
+++ b/go.sum
@@ -21,11 +21,10 @@ filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmG
 filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
 github.com/99designs/gqlgen v0.17.43 h1:I4SYg6ahjowErAQcHFVKy5EcWuwJ3+Xw9z2fLpuFCPo=
 github.com/99designs/gqlgen v0.17.43/go.mod h1:lO0Zjy8MkZgBdv4T1U91x09r0e0WFOdhVUutlQs1Rsc=
-github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
-github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
-github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w=
 github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI=
 github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
@@ -49,15 +48,15 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
 github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/anacrolix/chansync v0.3.0 h1:lRu9tbeuw3wl+PhMu/r+JJCRu5ArFXIluOgdF0ao6/U=
 github.com/anacrolix/chansync v0.3.0/go.mod h1:DZsatdsdXxD0WiwcGl0nJVwyjCKMDv+knl1q2iBjA2k=
-github.com/anacrolix/dht/v2 v2.21.0 h1:8nzI+faaynY9jOKmVgdmBZVrTo8B7ZE/LKEgN3Vl/Bs=
-github.com/anacrolix/dht/v2 v2.21.0/go.mod h1:SDGC+sEs1pnO2sJGYuhvIis7T8749dDHNfcjtdH4e3g=
+github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444 h1:8V0K09lrGoeT2KRJNOtspA7q+OMxGwQqK/Ug0IiaaRE=
+github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444/go.mod h1:MctKM1HS5YYDb3F30NGJxLE+QPuqWoT5ReW/4jt8xew=
 github.com/anacrolix/envpprof v0.0.0-20180404065416-323002cec2fa/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
 github.com/anacrolix/envpprof v1.0.0/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
 github.com/anacrolix/envpprof v1.1.0/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4=
 github.com/anacrolix/envpprof v1.3.0 h1:WJt9bpuT7A/CDCxPOv/eeZqHWlle/Y0keJUvc6tcJDk=
 github.com/anacrolix/envpprof v1.3.0/go.mod h1:7QIG4CaX1uexQ3tqd5+BRa/9e2D02Wcertl6Yh0jCB0=
-github.com/anacrolix/generics v0.0.0-20230816105729-c755655aee45 h1:Kmcl3I9K2+5AdnnR7hvrnVT0TLeFWWMa9bxnm55aVIg=
-github.com/anacrolix/generics v0.0.0-20230816105729-c755655aee45/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8=
+github.com/anacrolix/generics v0.0.0-20230911070922-5dd7545c6b13 h1:qwOprPTDMM3BASJRf84mmZnTXRsPGGJ8xoHKQS7m3so=
+github.com/anacrolix/generics v0.0.0-20230911070922-5dd7545c6b13/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8=
 github.com/anacrolix/go-libutp v1.3.1 h1:idJzreNLl+hNjGC3ZnUOjujEaryeOGgkwHLqSGoige0=
 github.com/anacrolix/go-libutp v1.3.1/go.mod h1:heF41EC8kN0qCLMokLBVkB8NXiLwx3t8R8810MTNI5o=
 github.com/anacrolix/log v0.3.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU=
@@ -96,8 +95,8 @@ github.com/anacrolix/sync v0.5.1/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DC
 github.com/anacrolix/tagflag v0.0.0-20180109131632-2146c8d41bf0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw=
 github.com/anacrolix/tagflag v1.0.0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw=
 github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8=
-github.com/anacrolix/torrent v1.54.0 h1:sl+2J1pHjJWq6+5G861+Yc74k2XTc/m8ijaMQR/8+2k=
-github.com/anacrolix/torrent v1.54.0/go.mod h1:is8GNob5qDeZ5Kq+pKPiE2xqYUi1ms7IgSB+CftZETk=
+github.com/anacrolix/torrent v1.55.0 h1:s9yh/YGdPmbN9dTa+0Inh2dLdrLQRvEAj1jdFW/Hdd8=
+github.com/anacrolix/torrent v1.55.0/go.mod h1:sBdZHBSZNj4de0m+EbYg7vvs/G/STubxu/GzzNbojsE=
 github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96 h1:QAVZ3pN/J4/UziniAhJR2OZ9Ox5kOY2053tBbbqUPYA=
 github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96/go.mod h1:Wa6n8cYIdaG35x15aH3Zy6d03f7P728QfdcDeD/IEOs=
 github.com/anacrolix/utp v0.1.0 h1:FOpQOmIwYsnENnz7tAGohA+r6iXpRjrq8ssKSre2Cp4=
@@ -109,7 +108,6 @@ github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHG
 github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
 github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
 github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
-github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
 github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
 github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
 github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI=
@@ -137,6 +135,8 @@ github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1
 github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
 github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
 github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -147,18 +147,13 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
-github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
-github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
-github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
-github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
 github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/dgraph-io/badger v1.6.0 h1:DshxFxZWXUcO0xX476VJC07Xsr6ZCBVRHKZ93Oh7Evo=
-github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
 github.com/dgraph-io/badger/v4 v4.2.0 h1:kJrlajbXXL9DFTNuhhu9yCx7JJa4qpYWxtE8BzuWsEs=
 github.com/dgraph-io/badger/v4 v4.2.0/go.mod h1:qfCqhPoWDFJRx1gp5QwwyGo8xk1lbHUxvK9nK0OGAak=
 github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
@@ -176,7 +171,9 @@ github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1
 github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
 github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ=
 github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
 github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
@@ -215,8 +212,8 @@ github.com/go-llsqlite/crawshaw v0.4.0/go.mod h1:/YJdV7uBQaYDE0fwe4z3wwJIZBJxdYz
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
-github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
+github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
@@ -233,18 +230,18 @@ github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg
 github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
-github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
-github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
 github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
 github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M=
+github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
-github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
+github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE=
+github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ=
 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
@@ -262,25 +259,30 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x
 github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
 github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
 github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
 github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
-github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
+github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
 github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
-github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw=
-github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
+github.com/google/flatbuffers v2.0.8+incompatible h1:ivUb1cGomAB101ZM1T0nOiWz9pSrTMoa9+EiY7igmkM=
+github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
@@ -290,6 +292,7 @@ github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OI
 github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
 github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -311,7 +314,6 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
 github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
-github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
 github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
@@ -322,7 +324,6 @@ github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq
 github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
 github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@@ -368,15 +369,12 @@ github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
 github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
 github.com/lmittmann/tint v1.0.4 h1:LeYihpJ9hyGvE0w+K2okPTGUdVLfng1+nDNVR4vWISc=
 github.com/lmittmann/tint v1.0.4/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
-github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
-github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
-github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
 github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
@@ -406,24 +404,10 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J
 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
 github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
 github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
-github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
 github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
 github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
 github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
-github.com/philippgille/gokv v0.0.0-20191001201555-5ac9a20de634/go.mod h1:OCoWPt+mbYuTO1FUVrQ2SxQU0oaaHBsn6lRhFX3JHOc=
-github.com/philippgille/gokv v0.5.1-0.20191011213304-eb77f15b9c61/go.mod h1:OCoWPt+mbYuTO1FUVrQ2SxQU0oaaHBsn6lRhFX3JHOc=
-github.com/philippgille/gokv v0.6.0 h1:fNEx/tSwV73nzlYd3iRYB8F+SEVJNNFzH1gsaT8SK2c=
-github.com/philippgille/gokv v0.6.0/go.mod h1:tjXRFw9xDHgxLS8WJdfYotKGWp8TWqu4RdXjMDG/XBo=
-github.com/philippgille/gokv/badgerdb v0.6.0 h1:4Qigf2SpyXLF8KaM5nA5/D/0aD/bZevuAnrW4ZsDsjA=
-github.com/philippgille/gokv/badgerdb v0.6.0/go.mod h1:3u2avs8gtmCc0R0Bw4jKV8aaDfLb5V9JToSASyhpFGM=
-github.com/philippgille/gokv/encoding v0.0.0-20191011213304-eb77f15b9c61/go.mod h1:SjxSrCoeYrYn85oTtroyG1ePY8aE72nvLQlw8IYwAN8=
-github.com/philippgille/gokv/encoding v0.6.0 h1:P1TN+Aulpd6Qd7qcLqgPwoxzOQ42UHBXOovWvFxJRI8=
-github.com/philippgille/gokv/encoding v0.6.0/go.mod h1:/yKvq2BKJlKJsH7KMDrhDlEw2Pt3V1nKyFhs4iOqz5U=
-github.com/philippgille/gokv/test v0.0.0-20191011213304-eb77f15b9c61 h1:4tVyBgfpK0NSqu7tNZTwYfC/pbyWUR2y+O7mxEg5BTQ=
-github.com/philippgille/gokv/test v0.0.0-20191011213304-eb77f15b9c61/go.mod h1:EUc+s9ONc1+VOr9NUEd8S0YbGRrQd/gz/p+2tvwt12s=
-github.com/philippgille/gokv/util v0.0.0-20191011213304-eb77f15b9c61 h1:ril/jI0JgXNjPWwDkvcRxlZ09kgHXV2349xChjbsQ4o=
-github.com/philippgille/gokv/util v0.0.0-20191011213304-eb77f15b9c61/go.mod h1:2dBhsJgY/yVIkjY5V3AnDUxUbEPzT6uQ3LvoVT8TR20=
 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
 github.com/pierrec/lz4/v4 v4.1.19 h1:tYLzDnjDXh9qIxSTKHwXwOYmm9d887Y7Y1ZkyXYHAN4=
 github.com/pierrec/lz4/v4 v4.1.19/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
@@ -505,9 +489,12 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE
 github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
 github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
 github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
+github.com/royalcat/kv v0.0.0-20240316120131-b774a9bff6f7 h1:fmBTD0RaTvWbd6KrgLVXSDUJ4dWTPOFUdkdHp+kYvRM=
+github.com/royalcat/kv v0.0.0-20240316120131-b774a9bff6f7/go.mod h1:Ff0Z/r1H3ojacpEe8SashMKJx6YCIhWrYtpdV8Y/k3A=
+github.com/royalcat/kv v0.0.0-20240316134516-1bb692abce73 h1:zeFE8Nx11oD6In+f+VDYwGH72t7NV6L5dqaNbDIhB1E=
+github.com/royalcat/kv v0.0.0-20240316134516-1bb692abce73/go.mod h1:Ff0Z/r1H3ojacpEe8SashMKJx6YCIhWrYtpdV8Y/k3A=
 github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 h1:Lt9DzQALzHoDwMBGJ6v8ObDPR0dzr2a6sXTB1Fq7IHs=
 github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA=
-github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
@@ -526,12 +513,8 @@ github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:X
 github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs=
 github.com/sosodev/duration v1.1.0 h1:kQcaiGbJaIsRqgQy7VGlZrVw1giWO+lDoX3MCPnpVO4=
 github.com/sosodev/duration v1.1.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
-github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
-github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
-github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
-github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
-github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
@@ -558,7 +541,6 @@ github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDW
 github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
 github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
 github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
-github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
 github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
 github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
 github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
@@ -574,7 +556,6 @@ github.com/willscott/go-nfs v0.0.2 h1:BaBp1CpGDMooCT6bCgX6h6ZwgPcTMST4yToYZ9byee
 github.com/willscott/go-nfs v0.0.2/go.mod h1:SvullWeHxr/924WQNbUaZqtluBt2vuZ61g6yAV+xj7w=
 github.com/willscott/go-nfs-client v0.0.0-20240104095149-b44639837b00 h1:U0DnHRZFzoIV1oFEZczg5XyPut9yxk9jjtax/9Bxr/o=
 github.com/willscott/go-nfs-client v0.0.0-20240104095149-b44639837b00/go.mod h1:Tq++Lr/FgiS3X48q5FETemXiSLGuYMQT2sPjYNPJSwA=
-github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -588,19 +569,20 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
 go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
 go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0=
-go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
+go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
+go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
 go.opentelemetry.io/otel v1.8.0 h1:zcvBFizPbpa1q7FehvFiHbQwGzmPILebO0tyqIR5Djg=
 go.opentelemetry.io/otel v1.8.0/go.mod h1:2pkj+iMj0o03Y+cW6/m8Y4WkRdYN3AvCXCnzRMp9yvM=
 go.opentelemetry.io/otel/trace v1.8.0 h1:cSy0DF9eGI5WIfNwZ1q2iUyGj00tGzP24dE1lOlHrfY=
 go.opentelemetry.io/otel/trace v1.8.0/go.mod h1:0Bt3PXY8w+3pheS3hQUt+wow8b1ojPaTBoTCh2zIFI4=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
 go4.org v0.0.0-20200411211856-f5505b9728dd h1:BNJlw5kRTzdmyfh5U8F93HA2OwkP7ZGwA51eJ/0wKOU=
 go4.org v0.0.0-20200411211856-f5505b9728dd/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZMZXMSBGhxRdsvzbkg=
 golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
 golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
 golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -660,13 +642,13 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
 golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20191011234655-491137f69257/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@@ -703,7 +685,6 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -712,11 +693,9 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -831,20 +810,26 @@ google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvx
 google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
 google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
 google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
 google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
 google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
 google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
 google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
diff --git a/graphql/mutation.graphql b/graphql/mutation.graphql
index 2d95198..80830ed 100644
--- a/graphql/mutation.graphql
+++ b/graphql/mutation.graphql
@@ -1,10 +1,20 @@
 type Mutation {
     validateTorrents(filter: TorrentFilter!): Boolean!
     cleanupTorrents(files: Boolean, dryRun: Boolean!): Int!
+    downloadTorrent(infohash: String!, file: String): DownloadTorrentResponse
 }
 
+
 input TorrentFilter @oneOf {
     everything: Boolean
     infohash: String
     # pathGlob: String!
+}
+
+type DownloadTorrentResponse {
+    task: Task
+}
+
+type Task {
+    id: ID!
 }
\ No newline at end of file
diff --git a/graphql/schema.graphql b/graphql/schema.graphql
index 7942192..9342a90 100644
--- a/graphql/schema.graphql
+++ b/graphql/schema.graphql
@@ -1,4 +1,5 @@
 directive @oneOf on INPUT_OBJECT | FIELD_DEFINITION
+directive @stream on FIELD_DEFINITION
 
 scalar DateTime
 
@@ -6,4 +7,3 @@ type Schema {
   query: Query
   mutation: Mutation
 }
-
diff --git a/graphql/subscription.graphql b/graphql/subscription.graphql
new file mode 100644
index 0000000..9df09ee
--- /dev/null
+++ b/graphql/subscription.graphql
@@ -0,0 +1,16 @@
+type Subscription {
+    taskProgress(taskID: ID!): Progress
+    torrentDownloadUpdates: TorrentProgress
+}
+
+
+type TorrentProgress implements Progress {
+    torrent: Torrent!
+    current: Int!
+    total: Int!
+}
+
+interface Progress {
+    current: Int!
+    total: Int!
+}
\ No newline at end of file
diff --git a/pkg/uuid/uuid.go b/pkg/uuid/uuid.go
new file mode 100644
index 0000000..923ef0e
--- /dev/null
+++ b/pkg/uuid/uuid.go
@@ -0,0 +1,102 @@
+package uuid
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"strconv"
+	"time"
+
+	fuuid "github.com/gofrs/uuid/v5"
+)
+
+var Nil = UUID{}
+
+type UUIDList = []UUID
+
+type UUID struct {
+	fuuid.UUID
+}
+
+func New() UUID {
+	return UUID{fuuid.Must(fuuid.NewV7())}
+}
+
+func NewFromTime(t time.Time) UUID {
+	gen := fuuid.NewGenWithOptions(
+		fuuid.WithEpochFunc(func() time.Time { return t }),
+	)
+	return UUID{fuuid.Must(gen.NewV7())}
+}
+
+func NewP() *UUID {
+	return &UUID{fuuid.Must(fuuid.NewV7())}
+}
+
+func FromString(text string) (UUID, error) {
+	u, err := fuuid.FromString(text)
+	if err != nil {
+		return Nil, err
+	}
+
+	return UUID{u}, nil
+}
+
+func MustFromString(text string) UUID {
+	u, err := fuuid.FromString(text)
+	if err != nil {
+		panic(err)
+	}
+
+	return UUID{u}
+}
+
+func FromBytes(input []byte) (UUID, error) {
+	u, err := fuuid.FromBytes(input)
+	if err != nil {
+		return Nil, err
+	}
+
+	return UUID{u}, nil
+}
+
+func (a *UUID) UnmarshalJSON(b []byte) error {
+	var s string
+	if err := json.Unmarshal(b, &s); err != nil {
+		return err
+	}
+
+	if s == "" {
+		a.UUID = fuuid.Nil
+		return nil
+	}
+
+	return a.UUID.Parse(s)
+}
+
+func (a UUID) MarshalJSON() ([]byte, error) {
+	if a.IsNil() {
+		return json.Marshal("")
+	}
+
+	return json.Marshal(a.UUID)
+}
+
+// UnmarshalGQL implements the graphql.Unmarshaler interface
+func (u *UUID) UnmarshalGQL(v interface{}) error {
+	id, ok := v.(string)
+	if !ok {
+		return fmt.Errorf("uuid must be a string")
+	}
+
+	return u.Parse(id)
+}
+
+// MarshalGQL implements the graphql.Marshaler interface
+func (u UUID) MarshalGQL(w io.Writer) {
+	b := []byte(strconv.Quote(u.String()))
+	_, err := w.Write(b)
+	if err != nil {
+		panic(err)
+	}
+}
diff --git a/src/delivery/graphql/generated.go b/src/delivery/graphql/generated.go
index e91e331..a4f51df 100644
--- a/src/delivery/graphql/generated.go
+++ b/src/delivery/graphql/generated.go
@@ -7,6 +7,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"io"
 	"strconv"
 	"sync"
 	"sync/atomic"
@@ -41,16 +42,23 @@ type Config struct {
 type ResolverRoot interface {
 	Mutation() MutationResolver
 	Query() QueryResolver
+	Subscription() SubscriptionResolver
 	Torrent() TorrentResolver
 }
 
 type DirectiveRoot struct {
-	OneOf func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error)
+	OneOf  func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error)
+	Stream func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error)
 }
 
 type ComplexityRoot struct {
+	DownloadTorrentResponse struct {
+		Task func(childComplexity int) int
+	}
+
 	Mutation struct {
 		CleanupTorrents  func(childComplexity int, files *bool, dryRun bool) int
+		DownloadTorrent  func(childComplexity int, infohash string, file *string) int
 		ValidateTorrents func(childComplexity int, filter model.TorrentFilter) int
 	}
 
@@ -63,6 +71,15 @@ type ComplexityRoot struct {
 		Query    func(childComplexity int) int
 	}
 
+	Subscription struct {
+		TaskProgress           func(childComplexity int, taskID string) int
+		TorrentDownloadUpdates func(childComplexity int) int
+	}
+
+	Task struct {
+		ID func(childComplexity int) int
+	}
+
 	Torrent struct {
 		BytesCompleted  func(childComplexity int) int
 		BytesMissing    func(childComplexity int) int
@@ -87,15 +104,26 @@ type ComplexityRoot struct {
 		IP           func(childComplexity int) int
 		Port         func(childComplexity int) int
 	}
+
+	TorrentProgress struct {
+		Current func(childComplexity int) int
+		Torrent func(childComplexity int) int
+		Total   func(childComplexity int) int
+	}
 }
 
 type MutationResolver interface {
 	ValidateTorrents(ctx context.Context, filter model.TorrentFilter) (bool, error)
 	CleanupTorrents(ctx context.Context, files *bool, dryRun bool) (int64, error)
+	DownloadTorrent(ctx context.Context, infohash string, file *string) (*model.DownloadTorrentResponse, error)
 }
 type QueryResolver interface {
 	Torrents(ctx context.Context, filter *model.TorrentsFilter, pagination *model.Pagination) ([]*model.Torrent, error)
 }
+type SubscriptionResolver interface {
+	TaskProgress(ctx context.Context, taskID string) (<-chan model.Progress, error)
+	TorrentDownloadUpdates(ctx context.Context) (<-chan *model.TorrentProgress, error)
+}
 type TorrentResolver interface {
 	Name(ctx context.Context, obj *model.Torrent) (string, error)
 
@@ -123,6 +151,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 	_ = ec
 	switch typeName + "." + field {
 
+	case "DownloadTorrentResponse.task":
+		if e.complexity.DownloadTorrentResponse.Task == nil {
+			break
+		}
+
+		return e.complexity.DownloadTorrentResponse.Task(childComplexity), true
+
 	case "Mutation.cleanupTorrents":
 		if e.complexity.Mutation.CleanupTorrents == nil {
 			break
@@ -135,6 +170,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.Mutation.CleanupTorrents(childComplexity, args["files"].(*bool), args["dryRun"].(bool)), true
 
+	case "Mutation.downloadTorrent":
+		if e.complexity.Mutation.DownloadTorrent == nil {
+			break
+		}
+
+		args, err := ec.field_Mutation_downloadTorrent_args(context.TODO(), rawArgs)
+		if err != nil {
+			return 0, false
+		}
+
+		return e.complexity.Mutation.DownloadTorrent(childComplexity, args["infohash"].(string), args["file"].(*string)), true
+
 	case "Mutation.validateTorrents":
 		if e.complexity.Mutation.ValidateTorrents == nil {
 			break
@@ -173,6 +220,32 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.Schema.Query(childComplexity), true
 
+	case "Subscription.taskProgress":
+		if e.complexity.Subscription.TaskProgress == nil {
+			break
+		}
+
+		args, err := ec.field_Subscription_taskProgress_args(context.TODO(), rawArgs)
+		if err != nil {
+			return 0, false
+		}
+
+		return e.complexity.Subscription.TaskProgress(childComplexity, args["taskID"].(string)), true
+
+	case "Subscription.torrentDownloadUpdates":
+		if e.complexity.Subscription.TorrentDownloadUpdates == nil {
+			break
+		}
+
+		return e.complexity.Subscription.TorrentDownloadUpdates(childComplexity), true
+
+	case "Task.id":
+		if e.complexity.Task.ID == nil {
+			break
+		}
+
+		return e.complexity.Task.ID(childComplexity), true
+
 	case "Torrent.bytesCompleted":
 		if e.complexity.Torrent.BytesCompleted == nil {
 			break
@@ -285,6 +358,27 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.TorrentPeer.Port(childComplexity), true
 
+	case "TorrentProgress.current":
+		if e.complexity.TorrentProgress.Current == nil {
+			break
+		}
+
+		return e.complexity.TorrentProgress.Current(childComplexity), true
+
+	case "TorrentProgress.torrent":
+		if e.complexity.TorrentProgress.Torrent == nil {
+			break
+		}
+
+		return e.complexity.TorrentProgress.Torrent(childComplexity), true
+
+	case "TorrentProgress.total":
+		if e.complexity.TorrentProgress.Total == nil {
+			break
+		}
+
+		return e.complexity.TorrentProgress.Total(childComplexity), true
+
 	}
 	return 0, false
 }
@@ -345,6 +439,23 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
 			var buf bytes.Buffer
 			data.MarshalGQL(&buf)
 
+			return &graphql.Response{
+				Data: buf.Bytes(),
+			}
+		}
+	case ast.Subscription:
+		next := ec._Subscription(ctx, rc.Operation.SelectionSet)
+
+		var buf bytes.Buffer
+		return func(ctx context.Context) *graphql.Response {
+			buf.Reset()
+			data := next(ctx)
+
+			if data == nil {
+				return nil
+			}
+			data.MarshalGQL(&buf)
+
 			return &graphql.Response{
 				Data: buf.Bytes(),
 			}
@@ -400,12 +511,22 @@ var sources = []*ast.Source{
 	{Name: "../../../graphql/mutation.graphql", Input: `type Mutation {
     validateTorrents(filter: TorrentFilter!): Boolean!
     cleanupTorrents(files: Boolean, dryRun: Boolean!): Int!
+    downloadTorrent(infohash: String!, file: String): DownloadTorrentResponse
 }
 
+
 input TorrentFilter @oneOf {
     everything: Boolean
     infohash: String
     # pathGlob: String!
+}
+
+type DownloadTorrentResponse {
+    task: Task
+}
+
+type Task {
+    id: ID!
 }`, BuiltIn: false},
 	{Name: "../../../graphql/query.graphql", Input: `type Query {
   torrents(filter: TorrentsFilter, pagination: Pagination): [Torrent!]!
@@ -451,6 +572,7 @@ input BooleanFilter @oneOf {
   eq: Boolean
 }`, BuiltIn: false},
 	{Name: "../../../graphql/schema.graphql", Input: `directive @oneOf on INPUT_OBJECT | FIELD_DEFINITION
+directive @stream on FIELD_DEFINITION
 
 scalar DateTime
 
@@ -460,6 +582,22 @@ type Schema {
 }
 
 `, BuiltIn: false},
+	{Name: "../../../graphql/subscription.graphql", Input: `type Subscription {
+    taskProgress(taskID: ID!): Progress
+    torrentDownloadUpdates: TorrentProgress
+}
+
+
+type TorrentProgress implements Progress {
+    torrent: Torrent!
+    current: Int!
+    total: Int!
+}
+
+interface Progress {
+    current: Int!
+    total: Int!
+}`, BuiltIn: false},
 	{Name: "../../../graphql/types/torrent.graphql", Input: `type Torrent {
   name: String!
   infohash: String!
@@ -515,6 +653,30 @@ func (ec *executionContext) field_Mutation_cleanupTorrents_args(ctx context.Cont
 	return args, nil
 }
 
+func (ec *executionContext) field_Mutation_downloadTorrent_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
+	var err error
+	args := map[string]interface{}{}
+	var arg0 string
+	if tmp, ok := rawArgs["infohash"]; ok {
+		ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("infohash"))
+		arg0, err = ec.unmarshalNString2string(ctx, tmp)
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["infohash"] = arg0
+	var arg1 *string
+	if tmp, ok := rawArgs["file"]; ok {
+		ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("file"))
+		arg1, err = ec.unmarshalOString2ᚖstring(ctx, tmp)
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["file"] = arg1
+	return args, nil
+}
+
 func (ec *executionContext) field_Mutation_validateTorrents_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
 	var err error
 	args := map[string]interface{}{}
@@ -569,6 +731,21 @@ func (ec *executionContext) field_Query_torrents_args(ctx context.Context, rawAr
 	return args, nil
 }
 
+func (ec *executionContext) field_Subscription_taskProgress_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
+	var err error
+	args := map[string]interface{}{}
+	var arg0 string
+	if tmp, ok := rawArgs["taskID"]; ok {
+		ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("taskID"))
+		arg0, err = ec.unmarshalNID2string(ctx, tmp)
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["taskID"] = arg0
+	return args, nil
+}
+
 func (ec *executionContext) field___Type_enumValues_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
 	var err error
 	args := map[string]interface{}{}
@@ -607,6 +784,51 @@ func (ec *executionContext) field___Type_fields_args(ctx context.Context, rawArg
 
 // region    **************************** field.gotpl *****************************
 
+func (ec *executionContext) _DownloadTorrentResponse_task(ctx context.Context, field graphql.CollectedField, obj *model.DownloadTorrentResponse) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_DownloadTorrentResponse_task(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Task, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(*model.Task)
+	fc.Result = res
+	return ec.marshalOTask2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTask(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_DownloadTorrentResponse_task(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "DownloadTorrentResponse",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			switch field.Name {
+			case "id":
+				return ec.fieldContext_Task_id(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type Task", field.Name)
+		},
+	}
+	return fc, nil
+}
+
 func (ec *executionContext) _Mutation_validateTorrents(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
 	fc, err := ec.fieldContext_Mutation_validateTorrents(ctx, field)
 	if err != nil {
@@ -717,6 +939,62 @@ func (ec *executionContext) fieldContext_Mutation_cleanupTorrents(ctx context.Co
 	return fc, nil
 }
 
+func (ec *executionContext) _Mutation_downloadTorrent(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_Mutation_downloadTorrent(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return ec.resolvers.Mutation().DownloadTorrent(rctx, fc.Args["infohash"].(string), fc.Args["file"].(*string))
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(*model.DownloadTorrentResponse)
+	fc.Result = res
+	return ec.marshalODownloadTorrentResponse2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐDownloadTorrentResponse(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Mutation_downloadTorrent(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "Mutation",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: true,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			switch field.Name {
+			case "task":
+				return ec.fieldContext_DownloadTorrentResponse_task(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type DownloadTorrentResponse", field.Name)
+		},
+	}
+	defer func() {
+		if r := recover(); r != nil {
+			err = ec.Recover(ctx, r)
+			ec.Error(ctx, err)
+		}
+	}()
+	ctx = graphql.WithFieldContext(ctx, fc)
+	if fc.Args, err = ec.field_Mutation_downloadTorrent_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
+		ec.Error(ctx, err)
+		return fc, err
+	}
+	return fc, nil
+}
+
 func (ec *executionContext) _Query_torrents(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
 	fc, err := ec.fieldContext_Query_torrents(ctx, field)
 	if err != nil {
@@ -986,6 +1264,8 @@ func (ec *executionContext) fieldContext_Schema_mutation(ctx context.Context, fi
 				return ec.fieldContext_Mutation_validateTorrents(ctx, field)
 			case "cleanupTorrents":
 				return ec.fieldContext_Mutation_cleanupTorrents(ctx, field)
+			case "downloadTorrent":
+				return ec.fieldContext_Mutation_downloadTorrent(ctx, field)
 			}
 			return nil, fmt.Errorf("no field named %q was found under type Mutation", field.Name)
 		},
@@ -993,6 +1273,179 @@ func (ec *executionContext) fieldContext_Schema_mutation(ctx context.Context, fi
 	return fc, nil
 }
 
+func (ec *executionContext) _Subscription_taskProgress(ctx context.Context, field graphql.CollectedField) (ret func(ctx context.Context) graphql.Marshaler) {
+	fc, err := ec.fieldContext_Subscription_taskProgress(ctx, field)
+	if err != nil {
+		return nil
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = nil
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return ec.resolvers.Subscription().TaskProgress(rctx, fc.Args["taskID"].(string))
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return nil
+	}
+	if resTmp == nil {
+		return nil
+	}
+	return func(ctx context.Context) graphql.Marshaler {
+		select {
+		case res, ok := <-resTmp.(<-chan model.Progress):
+			if !ok {
+				return nil
+			}
+			return graphql.WriterFunc(func(w io.Writer) {
+				w.Write([]byte{'{'})
+				graphql.MarshalString(field.Alias).MarshalGQL(w)
+				w.Write([]byte{':'})
+				ec.marshalOProgress2gitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐProgress(ctx, field.Selections, res).MarshalGQL(w)
+				w.Write([]byte{'}'})
+			})
+		case <-ctx.Done():
+			return nil
+		}
+	}
+}
+
+func (ec *executionContext) fieldContext_Subscription_taskProgress(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "Subscription",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: true,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("FieldContext.Child cannot be called on type INTERFACE")
+		},
+	}
+	defer func() {
+		if r := recover(); r != nil {
+			err = ec.Recover(ctx, r)
+			ec.Error(ctx, err)
+		}
+	}()
+	ctx = graphql.WithFieldContext(ctx, fc)
+	if fc.Args, err = ec.field_Subscription_taskProgress_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
+		ec.Error(ctx, err)
+		return fc, err
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _Subscription_torrentDownloadUpdates(ctx context.Context, field graphql.CollectedField) (ret func(ctx context.Context) graphql.Marshaler) {
+	fc, err := ec.fieldContext_Subscription_torrentDownloadUpdates(ctx, field)
+	if err != nil {
+		return nil
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = nil
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return ec.resolvers.Subscription().TorrentDownloadUpdates(rctx)
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return nil
+	}
+	if resTmp == nil {
+		return nil
+	}
+	return func(ctx context.Context) graphql.Marshaler {
+		select {
+		case res, ok := <-resTmp.(<-chan *model.TorrentProgress):
+			if !ok {
+				return nil
+			}
+			return graphql.WriterFunc(func(w io.Writer) {
+				w.Write([]byte{'{'})
+				graphql.MarshalString(field.Alias).MarshalGQL(w)
+				w.Write([]byte{':'})
+				ec.marshalOTorrentProgress2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentProgress(ctx, field.Selections, res).MarshalGQL(w)
+				w.Write([]byte{'}'})
+			})
+		case <-ctx.Done():
+			return nil
+		}
+	}
+}
+
+func (ec *executionContext) fieldContext_Subscription_torrentDownloadUpdates(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "Subscription",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: true,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			switch field.Name {
+			case "torrent":
+				return ec.fieldContext_TorrentProgress_torrent(ctx, field)
+			case "current":
+				return ec.fieldContext_TorrentProgress_current(ctx, field)
+			case "total":
+				return ec.fieldContext_TorrentProgress_total(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type TorrentProgress", field.Name)
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _Task_id(ctx context.Context, field graphql.CollectedField, obj *model.Task) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_Task_id(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.ID, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(string)
+	fc.Result = res
+	return ec.marshalNID2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Task_id(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "Task",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type ID does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
 func (ec *executionContext) _Torrent_name(ctx context.Context, field graphql.CollectedField, obj *model.Torrent) (ret graphql.Marshaler) {
 	fc, err := ec.fieldContext_Torrent_name(ctx, field)
 	if err != nil {
@@ -1725,6 +2178,156 @@ func (ec *executionContext) fieldContext_TorrentPeer_clientName(ctx context.Cont
 	return fc, nil
 }
 
+func (ec *executionContext) _TorrentProgress_torrent(ctx context.Context, field graphql.CollectedField, obj *model.TorrentProgress) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentProgress_torrent(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Torrent, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(*model.Torrent)
+	fc.Result = res
+	return ec.marshalNTorrent2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrent(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_TorrentProgress_torrent(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "TorrentProgress",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			switch field.Name {
+			case "name":
+				return ec.fieldContext_Torrent_name(ctx, field)
+			case "infohash":
+				return ec.fieldContext_Torrent_infohash(ctx, field)
+			case "bytesCompleted":
+				return ec.fieldContext_Torrent_bytesCompleted(ctx, field)
+			case "torrentFilePath":
+				return ec.fieldContext_Torrent_torrentFilePath(ctx, field)
+			case "bytesMissing":
+				return ec.fieldContext_Torrent_bytesMissing(ctx, field)
+			case "files":
+				return ec.fieldContext_Torrent_files(ctx, field)
+			case "excludedFiles":
+				return ec.fieldContext_Torrent_excludedFiles(ctx, field)
+			case "peers":
+				return ec.fieldContext_Torrent_peers(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type Torrent", field.Name)
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _TorrentProgress_current(ctx context.Context, field graphql.CollectedField, obj *model.TorrentProgress) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentProgress_current(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Current, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(int64)
+	fc.Result = res
+	return ec.marshalNInt2int64(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_TorrentProgress_current(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "TorrentProgress",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Int does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _TorrentProgress_total(ctx context.Context, field graphql.CollectedField, obj *model.TorrentProgress) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentProgress_total(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Total, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(int64)
+	fc.Result = res
+	return ec.marshalNInt2int64(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_TorrentProgress_total(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "TorrentProgress",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Int does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
 func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) {
 	fc, err := ec.fieldContext___Directive_name(ctx, field)
 	if err != nil {
@@ -4058,10 +4661,62 @@ func (ec *executionContext) unmarshalInputTorrentsFilter(ctx context.Context, ob
 
 // region    ************************** interface.gotpl ***************************
 
+func (ec *executionContext) _Progress(ctx context.Context, sel ast.SelectionSet, obj model.Progress) graphql.Marshaler {
+	switch obj := (obj).(type) {
+	case nil:
+		return graphql.Null
+	case model.TorrentProgress:
+		return ec._TorrentProgress(ctx, sel, &obj)
+	case *model.TorrentProgress:
+		if obj == nil {
+			return graphql.Null
+		}
+		return ec._TorrentProgress(ctx, sel, obj)
+	default:
+		panic(fmt.Errorf("unexpected type %T", obj))
+	}
+}
+
 // endregion ************************** interface.gotpl ***************************
 
 // region    **************************** object.gotpl ****************************
 
+var downloadTorrentResponseImplementors = []string{"DownloadTorrentResponse"}
+
+func (ec *executionContext) _DownloadTorrentResponse(ctx context.Context, sel ast.SelectionSet, obj *model.DownloadTorrentResponse) graphql.Marshaler {
+	fields := graphql.CollectFields(ec.OperationContext, sel, downloadTorrentResponseImplementors)
+
+	out := graphql.NewFieldSet(fields)
+	deferred := make(map[string]*graphql.FieldSet)
+	for i, field := range fields {
+		switch field.Name {
+		case "__typename":
+			out.Values[i] = graphql.MarshalString("DownloadTorrentResponse")
+		case "task":
+			out.Values[i] = ec._DownloadTorrentResponse_task(ctx, field, obj)
+		default:
+			panic("unknown field " + strconv.Quote(field.Name))
+		}
+	}
+	out.Dispatch(ctx)
+	if out.Invalids > 0 {
+		return graphql.Null
+	}
+
+	atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+	for label, dfs := range deferred {
+		ec.processDeferredGroup(graphql.DeferredGroup{
+			Label:    label,
+			Path:     graphql.GetPath(ctx),
+			FieldSet: dfs,
+			Context:  ctx,
+		})
+	}
+
+	return out
+}
+
 var mutationImplementors = []string{"Mutation"}
 
 func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler {
@@ -4095,6 +4750,10 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
 			if out.Values[i] == graphql.Null {
 				out.Invalids++
 			}
+		case "downloadTorrent":
+			out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
+				return ec._Mutation_downloadTorrent(ctx, field)
+			})
 		default:
 			panic("unknown field " + strconv.Quote(field.Name))
 		}
@@ -4228,6 +4887,67 @@ func (ec *executionContext) _Schema(ctx context.Context, sel ast.SelectionSet, o
 	return out
 }
 
+var subscriptionImplementors = []string{"Subscription"}
+
+func (ec *executionContext) _Subscription(ctx context.Context, sel ast.SelectionSet) func(ctx context.Context) graphql.Marshaler {
+	fields := graphql.CollectFields(ec.OperationContext, sel, subscriptionImplementors)
+	ctx = graphql.WithFieldContext(ctx, &graphql.FieldContext{
+		Object: "Subscription",
+	})
+	if len(fields) != 1 {
+		ec.Errorf(ctx, "must subscribe to exactly one stream")
+		return nil
+	}
+
+	switch fields[0].Name {
+	case "taskProgress":
+		return ec._Subscription_taskProgress(ctx, fields[0])
+	case "torrentDownloadUpdates":
+		return ec._Subscription_torrentDownloadUpdates(ctx, fields[0])
+	default:
+		panic("unknown field " + strconv.Quote(fields[0].Name))
+	}
+}
+
+var taskImplementors = []string{"Task"}
+
+func (ec *executionContext) _Task(ctx context.Context, sel ast.SelectionSet, obj *model.Task) graphql.Marshaler {
+	fields := graphql.CollectFields(ec.OperationContext, sel, taskImplementors)
+
+	out := graphql.NewFieldSet(fields)
+	deferred := make(map[string]*graphql.FieldSet)
+	for i, field := range fields {
+		switch field.Name {
+		case "__typename":
+			out.Values[i] = graphql.MarshalString("Task")
+		case "id":
+			out.Values[i] = ec._Task_id(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		default:
+			panic("unknown field " + strconv.Quote(field.Name))
+		}
+	}
+	out.Dispatch(ctx)
+	if out.Invalids > 0 {
+		return graphql.Null
+	}
+
+	atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+	for label, dfs := range deferred {
+		ec.processDeferredGroup(graphql.DeferredGroup{
+			Label:    label,
+			Path:     graphql.GetPath(ctx),
+			FieldSet: dfs,
+			Context:  ctx,
+		})
+	}
+
+	return out
+}
+
 var torrentImplementors = []string{"Torrent"}
 
 func (ec *executionContext) _Torrent(ctx context.Context, sel ast.SelectionSet, obj *model.Torrent) graphql.Marshaler {
@@ -4534,6 +5254,55 @@ func (ec *executionContext) _TorrentPeer(ctx context.Context, sel ast.SelectionS
 	return out
 }
 
+var torrentProgressImplementors = []string{"TorrentProgress", "Progress"}
+
+func (ec *executionContext) _TorrentProgress(ctx context.Context, sel ast.SelectionSet, obj *model.TorrentProgress) graphql.Marshaler {
+	fields := graphql.CollectFields(ec.OperationContext, sel, torrentProgressImplementors)
+
+	out := graphql.NewFieldSet(fields)
+	deferred := make(map[string]*graphql.FieldSet)
+	for i, field := range fields {
+		switch field.Name {
+		case "__typename":
+			out.Values[i] = graphql.MarshalString("TorrentProgress")
+		case "torrent":
+			out.Values[i] = ec._TorrentProgress_torrent(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "current":
+			out.Values[i] = ec._TorrentProgress_current(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "total":
+			out.Values[i] = ec._TorrentProgress_total(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		default:
+			panic("unknown field " + strconv.Quote(field.Name))
+		}
+	}
+	out.Dispatch(ctx)
+	if out.Invalids > 0 {
+		return graphql.Null
+	}
+
+	atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+	for label, dfs := range deferred {
+		ec.processDeferredGroup(graphql.DeferredGroup{
+			Label:    label,
+			Path:     graphql.GetPath(ctx),
+			FieldSet: dfs,
+			Context:  ctx,
+		})
+	}
+
+	return out
+}
+
 var __DirectiveImplementors = []string{"__Directive"}
 
 func (ec *executionContext) ___Directive(ctx context.Context, sel ast.SelectionSet, obj *introspection.Directive) graphql.Marshaler {
@@ -4890,6 +5659,21 @@ func (ec *executionContext) marshalNFloat2float64(ctx context.Context, sel ast.S
 	return graphql.WrapContextMarshaler(ctx, res)
 }
 
+func (ec *executionContext) unmarshalNID2string(ctx context.Context, v interface{}) (string, error) {
+	res, err := graphql.UnmarshalID(v)
+	return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalNID2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler {
+	res := graphql.MarshalID(v)
+	if res == graphql.Null {
+		if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+			ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+		}
+	}
+	return res
+}
+
 func (ec *executionContext) unmarshalNInt2int64(ctx context.Context, v interface{}) (int64, error) {
 	res, err := graphql.UnmarshalInt64(v)
 	return res, graphql.ErrorOnPath(ctx, err)
@@ -5382,6 +6166,13 @@ func (ec *executionContext) marshalODateTime2ᚖtimeᚐTime(ctx context.Context,
 	return res
 }
 
+func (ec *executionContext) marshalODownloadTorrentResponse2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐDownloadTorrentResponse(ctx context.Context, sel ast.SelectionSet, v *model.DownloadTorrentResponse) graphql.Marshaler {
+	if v == nil {
+		return graphql.Null
+	}
+	return ec._DownloadTorrentResponse(ctx, sel, v)
+}
+
 func (ec *executionContext) unmarshalOInt2ᚕint64ᚄ(ctx context.Context, v interface{}) ([]int64, error) {
 	if v == nil {
 		return nil, nil
@@ -5459,6 +6250,13 @@ func (ec *executionContext) unmarshalOPagination2ᚖgitᚗkmsignᚗruᚋroyalcat
 	return &res, graphql.ErrorOnPath(ctx, err)
 }
 
+func (ec *executionContext) marshalOProgress2gitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐProgress(ctx context.Context, sel ast.SelectionSet, v model.Progress) graphql.Marshaler {
+	if v == nil {
+		return graphql.Null
+	}
+	return ec._Progress(ctx, sel, v)
+}
+
 func (ec *executionContext) marshalOQuery2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐQuery(ctx context.Context, sel ast.SelectionSet, v *model.Query) graphql.Marshaler {
 	if v == nil {
 		return graphql.Null
@@ -5528,6 +6326,20 @@ func (ec *executionContext) unmarshalOStringFilter2ᚖgitᚗkmsignᚗruᚋroyalc
 	return &res, graphql.ErrorOnPath(ctx, err)
 }
 
+func (ec *executionContext) marshalOTask2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTask(ctx context.Context, sel ast.SelectionSet, v *model.Task) graphql.Marshaler {
+	if v == nil {
+		return graphql.Null
+	}
+	return ec._Task(ctx, sel, v)
+}
+
+func (ec *executionContext) marshalOTorrentProgress2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentProgress(ctx context.Context, sel ast.SelectionSet, v *model.TorrentProgress) graphql.Marshaler {
+	if v == nil {
+		return graphql.Null
+	}
+	return ec._TorrentProgress(ctx, sel, v)
+}
+
 func (ec *executionContext) unmarshalOTorrentsFilter2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentsFilter(ctx context.Context, v interface{}) (*model.TorrentsFilter, error) {
 	if v == nil {
 		return nil, nil
diff --git a/src/delivery/graphql/model/mappers.go b/src/delivery/graphql/model/mappers.go
index 1523130..e2227e0 100644
--- a/src/delivery/graphql/model/mappers.go
+++ b/src/delivery/graphql/model/mappers.go
@@ -1,6 +1,9 @@
 package model
 
-import "github.com/anacrolix/torrent"
+import (
+	"git.kmsign.ru/royalcat/tstor/src/host/controller"
+	"github.com/anacrolix/torrent"
+)
 
 func MapPeerSource(source torrent.PeerSource) string {
 	switch source {
@@ -22,3 +25,13 @@ func MapPeerSource(source torrent.PeerSource) string {
 		return "Unknown"
 	}
 }
+
+func MapTorrent(t *controller.Torrent) *Torrent {
+	return &Torrent{
+		Infohash:       t.InfoHash(),
+		Name:           t.Name(),
+		BytesCompleted: t.BytesCompleted(),
+		BytesMissing:   t.BytesMissing(),
+		T:              t,
+	}
+}
diff --git a/src/delivery/graphql/model/models_gen.go b/src/delivery/graphql/model/models_gen.go
index fec0890..deb4b6d 100644
--- a/src/delivery/graphql/model/models_gen.go
+++ b/src/delivery/graphql/model/models_gen.go
@@ -9,6 +9,12 @@ import (
 	"github.com/anacrolix/torrent"
 )
 
+type Progress interface {
+	IsProgress()
+	GetCurrent() int64
+	GetTotal() int64
+}
+
 type BooleanFilter struct {
 	Eq *bool `json:"eq,omitempty"`
 }
@@ -21,6 +27,10 @@ type DateTimeFilter struct {
 	Lte *time.Time `json:"lte,omitempty"`
 }
 
+type DownloadTorrentResponse struct {
+	Task *Task `json:"task,omitempty"`
+}
+
 type IntFilter struct {
 	Eq  *int64  `json:"eq,omitempty"`
 	Gt  *int64  `json:"gt,omitempty"`
@@ -52,6 +62,13 @@ type StringFilter struct {
 	In     []string `json:"in,omitempty"`
 }
 
+type Subscription struct {
+}
+
+type Task struct {
+	ID string `json:"id"`
+}
+
 type Torrent struct {
 	Name            string              `json:"name"`
 	Infohash        string              `json:"infohash"`
@@ -85,6 +102,16 @@ type TorrentPeer struct {
 	F            *torrent.PeerConn `json:"-"`
 }
 
+type TorrentProgress struct {
+	Torrent *Torrent `json:"torrent"`
+	Current int64    `json:"current"`
+	Total   int64    `json:"total"`
+}
+
+func (TorrentProgress) IsProgress()            {}
+func (this TorrentProgress) GetCurrent() int64 { return this.Current }
+func (this TorrentProgress) GetTotal() int64   { return this.Total }
+
 type TorrentsFilter struct {
 	Name           *StringFilter `json:"name,omitempty"`
 	BytesCompleted *IntFilter    `json:"bytesCompleted,omitempty"`
diff --git a/src/delivery/graphql/resolver/mutation.resolvers.go b/src/delivery/graphql/resolver/mutation.resolvers.go
index e6f16c6..210b7a6 100644
--- a/src/delivery/graphql/resolver/mutation.resolvers.go
+++ b/src/delivery/graphql/resolver/mutation.resolvers.go
@@ -7,8 +7,11 @@ package resolver
 import (
 	"context"
 
+	"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"
+	aih "github.com/anacrolix/torrent/types/infohash"
 )
 
 // ValidateTorrents is the resolver for the validateTorrents field.
@@ -58,6 +61,25 @@ func (r *mutationResolver) CleanupTorrents(ctx context.Context, files *bool, dry
 	}
 }
 
+// DownloadTorrent is the resolver for the downloadTorrent field.
+func (r *mutationResolver) DownloadTorrent(ctx context.Context, infohash string, file *string) (*model.DownloadTorrentResponse, error) {
+	f := ""
+	if file != nil {
+		f = *file
+	}
+
+	err := r.Service.Download(ctx, &service.TorrentDownloadTask{
+		ID:       uuid.New(),
+		InfoHash: aih.FromHexString(infohash),
+		File:     f,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	return &model.DownloadTorrentResponse{}, nil
+}
+
 // Mutation returns graph.MutationResolver implementation.
 func (r *Resolver) Mutation() graph.MutationResolver { return &mutationResolver{r} }
 
diff --git a/src/delivery/graphql/resolver/query.resolvers.go b/src/delivery/graphql/resolver/query.resolvers.go
index 9aecef7..b4687a7 100644
--- a/src/delivery/graphql/resolver/query.resolvers.go
+++ b/src/delivery/graphql/resolver/query.resolvers.go
@@ -52,13 +52,7 @@ func (r *queryResolver) Torrents(ctx context.Context, filter *model.TorrentsFilt
 
 	tr := []*model.Torrent{}
 	for _, t := range torrents {
-		d := &model.Torrent{
-			Infohash:       t.InfoHash(),
-			Name:           t.Name(),
-			BytesCompleted: t.BytesCompleted(),
-			BytesMissing:   t.BytesMissing(),
-			T:              t,
-		}
+		d := model.MapTorrent(t)
 
 		if !filterFunc(d) {
 			continue
diff --git a/src/delivery/graphql/resolver/subscription.resolvers.go b/src/delivery/graphql/resolver/subscription.resolvers.go
new file mode 100644
index 0000000..f30af50
--- /dev/null
+++ b/src/delivery/graphql/resolver/subscription.resolvers.go
@@ -0,0 +1,54 @@
+package resolver
+
+// This file will be automatically regenerated based on the schema, any resolver implementations
+// will be copied through when generating and any unknown code will be moved to the end.
+// Code generated by github.com/99designs/gqlgen version v0.17.43
+
+import (
+	"context"
+	"fmt"
+
+	graph "git.kmsign.ru/royalcat/tstor/src/delivery/graphql"
+	"git.kmsign.ru/royalcat/tstor/src/delivery/graphql/model"
+)
+
+// TaskProgress is the resolver for the taskProgress field.
+func (r *subscriptionResolver) TaskProgress(ctx context.Context, taskID string) (<-chan model.Progress, error) {
+	panic(fmt.Errorf("not implemented: TaskProgress - taskProgress"))
+}
+
+// TorrentDownloadUpdates is the resolver for the torrentDownloadUpdates field.
+func (r *subscriptionResolver) TorrentDownloadUpdates(ctx context.Context) (<-chan *model.TorrentProgress, error) {
+	out := make(chan *model.TorrentProgress)
+	progress, err := r.Service.DownloadProgress(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	go func() {
+		defer close(out)
+		for p := range progress {
+			if p.Torrent == nil {
+				fmt.Println("nil torrent")
+				continue
+			}
+			po := &model.TorrentProgress{
+				Torrent: model.MapTorrent(p.Torrent),
+				Current: p.Current,
+				Total:   p.Total,
+			}
+			select {
+			case <-ctx.Done():
+				return
+			case out <- po:
+			}
+		}
+	}()
+
+	return out, nil
+}
+
+// Subscription returns graph.SubscriptionResolver implementation.
+func (r *Resolver) Subscription() graph.SubscriptionResolver { return &subscriptionResolver{r} }
+
+type subscriptionResolver struct{ *Resolver }
diff --git a/src/delivery/graphql/resolver/torrent.resolvers.go b/src/delivery/graphql/resolver/torrent.resolvers.go
index 778fbf6..ef231ad 100644
--- a/src/delivery/graphql/resolver/torrent.resolvers.go
+++ b/src/delivery/graphql/resolver/torrent.resolvers.go
@@ -19,7 +19,7 @@ func (r *torrentResolver) Name(ctx context.Context, obj *model.Torrent) (string,
 // Files is the resolver for the files field.
 func (r *torrentResolver) Files(ctx context.Context, obj *model.Torrent) ([]*model.TorrentFile, error) {
 	out := []*model.TorrentFile{}
-	files, err := obj.T.Files()
+	files, err := obj.T.Files(ctx)
 	if err != nil {
 		return nil, err
 	}
@@ -37,17 +37,17 @@ func (r *torrentResolver) Files(ctx context.Context, obj *model.Torrent) ([]*mod
 // ExcludedFiles is the resolver for the excludedFiles field.
 func (r *torrentResolver) ExcludedFiles(ctx context.Context, obj *model.Torrent) ([]*model.TorrentFile, error) {
 	out := []*model.TorrentFile{}
-	files, err := obj.T.ExcludedFiles()
-	if err != nil {
-		return nil, err
-	}
-	for _, f := range files {
-		out = append(out, &model.TorrentFile{
-			Filename: f.DisplayPath(),
-			Size:     f.Length(),
-			F:        f,
-		})
-	}
+	// files, err := obj.T.ExcludedFiles()
+	// if err != nil {
+	// 	return nil, err
+	// }
+	// for _, f := range files {
+	// 	out = append(out, &model.TorrentFile{
+	// 		Filename: f.DisplayPath(),
+	// 		Size:     f.Length(),
+	// 		F:        f,
+	// 	})
+	// }
 	return out, nil
 }
 
diff --git a/src/export/nfs/cache.go b/src/export/nfs/cache.go
new file mode 100644
index 0000000..926bbda
--- /dev/null
+++ b/src/export/nfs/cache.go
@@ -0,0 +1,197 @@
+package nfs
+
+import (
+	"crypto/sha256"
+	"encoding/binary"
+	"io/fs"
+	"reflect"
+	"slices"
+
+	"github.com/willscott/go-nfs"
+
+	"github.com/go-git/go-billy/v5"
+	"github.com/google/uuid"
+	lru "github.com/hashicorp/golang-lru/v2"
+)
+
+// NewCachingHandler wraps a handler to provide a basic to/from-file handle cache.
+func NewCachingHandler(h nfs.Handler, limit int) nfs.Handler {
+	return NewCachingHandlerWithVerifierLimit(h, limit, limit)
+}
+
+// NewCachingHandlerWithVerifierLimit provides a basic to/from-file handle cache that can be tuned with a smaller cache of active directory listings.
+func NewCachingHandlerWithVerifierLimit(h nfs.Handler, limit int, verifierLimit int) nfs.Handler {
+	if limit < 2 || verifierLimit < 2 {
+		nfs.Log.Warnf("Caching handler created with insufficient cache to support directory listing", "size", limit, "verifiers", verifierLimit)
+	}
+	cache, _ := lru.New[uuid.UUID, entry](limit)
+	reverseCache := make(map[string][]uuid.UUID)
+	verifiers, _ := lru.New[uint64, verifier](verifierLimit)
+	return &CachingHandler{
+		Handler:         h,
+		activeHandles:   cache,
+		reverseHandles:  reverseCache,
+		activeVerifiers: verifiers,
+		cacheLimit:      limit,
+	}
+}
+
+// CachingHandler implements to/from handle via an LRU cache.
+type CachingHandler struct {
+	nfs.Handler
+	activeHandles   *lru.Cache[uuid.UUID, entry]
+	reverseHandles  map[string][]uuid.UUID
+	activeVerifiers *lru.Cache[uint64, verifier]
+	cacheLimit      int
+}
+
+type entry struct {
+	f billy.Filesystem
+	p []string
+}
+
+// ToHandle takes a file and represents it with an opaque handle to reference it.
+// In stateless nfs (when it's serving a unix fs) this can be the device + inode
+// but we can generalize with a stateful local cache of handed out IDs.
+func (c *CachingHandler) ToHandle(f billy.Filesystem, path []string) []byte {
+	joinedPath := f.Join(path...)
+
+	if handle := c.searchReverseCache(f, joinedPath); handle != nil {
+		return handle
+	}
+
+	id := uuid.New()
+
+	newPath := make([]string, len(path))
+
+	copy(newPath, path)
+	evictedKey, evictedPath, ok := c.activeHandles.GetOldest()
+	if evicted := c.activeHandles.Add(id, entry{f, newPath}); evicted && ok {
+		rk := evictedPath.f.Join(evictedPath.p...)
+		c.evictReverseCache(rk, evictedKey)
+	}
+
+	if _, ok := c.reverseHandles[joinedPath]; !ok {
+		c.reverseHandles[joinedPath] = []uuid.UUID{}
+	}
+	c.reverseHandles[joinedPath] = append(c.reverseHandles[joinedPath], id)
+	b, _ := id.MarshalBinary()
+
+	return b
+}
+
+// FromHandle converts from an opaque handle to the file it represents
+func (c *CachingHandler) FromHandle(fh []byte) (billy.Filesystem, []string, error) {
+	id, err := uuid.FromBytes(fh)
+	if err != nil {
+		return nil, []string{}, err
+	}
+
+	if f, ok := c.activeHandles.Get(id); ok {
+		for _, k := range c.activeHandles.Keys() {
+			candidate, _ := c.activeHandles.Peek(k)
+			if hasPrefix(f.p, candidate.p) {
+				_, _ = c.activeHandles.Get(k)
+			}
+		}
+
+		return f.f, slices.Clone(f.p), nil
+	}
+	return nil, []string{}, &nfs.NFSStatusError{NFSStatus: nfs.NFSStatusStale}
+}
+
+func (c *CachingHandler) searchReverseCache(f billy.Filesystem, path string) []byte {
+	uuids, exists := c.reverseHandles[path]
+
+	if !exists {
+		return nil
+	}
+
+	for _, id := range uuids {
+		if candidate, ok := c.activeHandles.Get(id); ok {
+			if reflect.DeepEqual(candidate.f, f) {
+				return id[:]
+			}
+		}
+	}
+
+	return nil
+}
+
+func (c *CachingHandler) evictReverseCache(path string, handle uuid.UUID) {
+	uuids, exists := c.reverseHandles[path]
+
+	if !exists {
+		return
+	}
+	for i, u := range uuids {
+		if u == handle {
+			uuids = append(uuids[:i], uuids[i+1:]...)
+			c.reverseHandles[path] = uuids
+			return
+		}
+	}
+}
+
+func (c *CachingHandler) InvalidateHandle(fs billy.Filesystem, handle []byte) error {
+	//Remove from cache
+	id, _ := uuid.FromBytes(handle)
+	entry, ok := c.activeHandles.Get(id)
+	if ok {
+		rk := entry.f.Join(entry.p...)
+		c.evictReverseCache(rk, id)
+	}
+	c.activeHandles.Remove(id)
+	return nil
+}
+
+// HandleLimit exports how many file handles can be safely stored by this cache.
+func (c *CachingHandler) HandleLimit() int {
+	return c.cacheLimit
+}
+
+func hasPrefix(path, prefix []string) bool {
+	if len(prefix) > len(path) {
+		return false
+	}
+	for i, e := range prefix {
+		if path[i] != e {
+			return false
+		}
+	}
+	return true
+}
+
+type verifier struct {
+	path     string
+	contents []fs.FileInfo
+}
+
+func hashPathAndContents(path string, contents []fs.FileInfo) uint64 {
+	//calculate a cookie-verifier.
+	vHash := sha256.New()
+
+	// Add the path to avoid collisions of directories with the same content
+	vHash.Write(binary.BigEndian.AppendUint64([]byte{}, uint64(len(path))))
+	vHash.Write([]byte(path))
+
+	for _, c := range contents {
+		vHash.Write([]byte(c.Name())) // Never fails according to the docs
+	}
+
+	verify := vHash.Sum(nil)[0:8]
+	return binary.BigEndian.Uint64(verify)
+}
+
+func (c *CachingHandler) VerifierFor(path string, contents []fs.FileInfo) uint64 {
+	id := hashPathAndContents(path, contents)
+	c.activeVerifiers.Add(id, verifier{path, contents})
+	return id
+}
+
+func (c *CachingHandler) DataForVerifier(path string, id uint64) []fs.FileInfo {
+	if cache, ok := c.activeVerifiers.Get(id); ok {
+		return cache.contents
+	}
+	return nil
+}
diff --git a/src/export/nfs/handler.go b/src/export/nfs/handler.go
index c722fb4..6baa241 100644
--- a/src/export/nfs/handler.go
+++ b/src/export/nfs/handler.go
@@ -17,7 +17,7 @@ func NewNFSv3Handler(fs vfs.Filesystem) (nfs.Handler, error) {
 	bfs := &billyFsWrapper{fs: fs, log: nfslog}
 	handler := nfshelper.NewNullAuthHandler(bfs)
 
-	cacheHelper := nfshelper.NewCachingHandler(handler, 1024*16)
+	cacheHelper := nfshelper.NewCachingHandler(handler, 1024)
 
 	//  cacheHelper := NewCachingHandler(handler)
 
diff --git a/src/host/controller/torrent.go b/src/host/controller/torrent.go
index 911a4d3..30aa6cc 100644
--- a/src/host/controller/torrent.go
+++ b/src/host/controller/torrent.go
@@ -1,6 +1,7 @@
 package controller
 
 import (
+	"context"
 	"slices"
 	"strings"
 
@@ -11,10 +12,10 @@ import (
 type Torrent struct {
 	torrentFilePath string
 	t               *torrent.Torrent
-	rep             *store.ExlcudedFiles
+	rep             *store.FilesMappings
 }
 
-func NewTorrent(t *torrent.Torrent, rep *store.ExlcudedFiles) *Torrent {
+func NewTorrent(t *torrent.Torrent, rep *store.FilesMappings) *Torrent {
 	return &Torrent{t: t, rep: rep}
 }
 
@@ -26,13 +27,13 @@ func (s *Torrent) Torrent() *torrent.Torrent {
 	return s.t
 }
 
-func (s *Torrent) Name() string {
-	<-s.t.GotInfo()
-	if name := s.t.Name(); name != "" {
+func (c *Torrent) Name() string {
+	<-c.t.GotInfo()
+	if name := c.t.Name(); name != "" {
 		return name
 	}
 
-	return s.InfoHash()
+	return c.InfoHash()
 }
 
 func (s *Torrent) InfoHash() string {
@@ -50,8 +51,13 @@ func (s *Torrent) BytesMissing() int64 {
 	return s.t.BytesMissing()
 }
 
-func (s *Torrent) Files() ([]*torrent.File, error) {
-	excludedFiles, err := s.rep.ExcludedFiles(s.t.InfoHash())
+func (s *Torrent) Length() int64 {
+	<-s.t.GotInfo()
+	return s.t.Length()
+}
+
+func (s *Torrent) Files(ctx context.Context) ([]*torrent.File, error) {
+	fileMappings, err := s.rep.FileMappings(ctx, s.t.InfoHash())
 	if err != nil {
 		return nil, err
 	}
@@ -60,25 +66,30 @@ func (s *Torrent) Files() ([]*torrent.File, error) {
 	files := s.t.Files()
 	files = slices.DeleteFunc(files, func(file *torrent.File) bool {
 		p := file.Path()
-
 		if strings.Contains(p, "/.pad/") {
-			return false
+			return true
 		}
-
-		if !slices.Contains(excludedFiles, p) {
-			return false
+		if target, ok := fileMappings[p]; ok && target == "" {
+			return true
 		}
-
-		return true
+		return false
 	})
 
-	for _, tf := range files {
-		s.isFileComplete(tf.BeginPieceIndex(), tf.EndPieceIndex())
-	}
-
 	return files, nil
 }
 
+func Map[T, U any](ts []T, f func(T) U) []U {
+	us := make([]U, len(ts))
+	for i := range ts {
+		us[i] = f(ts[i])
+	}
+	return us
+}
+
+func (s *Torrent) ExcludeFile(ctx context.Context, f *torrent.File) error {
+	return s.rep.ExcludeFile(ctx, f)
+}
+
 func (s *Torrent) isFileComplete(startIndex int, endIndex int) bool {
 	for i := startIndex; i < endIndex; i++ {
 		if !s.t.Piece(i).State().Complete {
@@ -88,35 +99,6 @@ func (s *Torrent) isFileComplete(startIndex int, endIndex int) bool {
 	return true
 }
 
-func (s *Torrent) ExcludedFiles() ([]*torrent.File, error) {
-	excludedFiles, err := s.rep.ExcludedFiles(s.t.InfoHash())
-	if err != nil {
-		return nil, err
-	}
-
-	<-s.t.GotInfo()
-	files := s.t.Files()
-	files = slices.DeleteFunc(files, func(file *torrent.File) bool {
-		p := file.Path()
-
-		if strings.Contains(p, "/.pad/") {
-			return false
-		}
-
-		if slices.Contains(excludedFiles, p) {
-			return false
-		}
-
-		return true
-	})
-
-	return files, nil
-}
-
-func (s *Torrent) ExcludeFile(f *torrent.File) error {
-	return s.rep.ExcludeFile(f)
-}
-
 func (s *Torrent) ValidateTorrent() error {
 	<-s.t.GotInfo()
 	s.t.VerifyData()
diff --git a/src/host/datastorage/piece_storage.go b/src/host/datastorage/piece_storage.go
new file mode 100644
index 0000000..26091f8
--- /dev/null
+++ b/src/host/datastorage/piece_storage.go
@@ -0,0 +1,172 @@
+package datastorage
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"os"
+	"path"
+
+	"git.kmsign.ru/royalcat/tstor/src/host/controller"
+	"github.com/anacrolix/torrent"
+	"github.com/anacrolix/torrent/metainfo"
+	"github.com/anacrolix/torrent/storage"
+	"github.com/anacrolix/torrent/types/infohash"
+	"github.com/hashicorp/go-multierror"
+	"github.com/royalcat/kv"
+)
+
+type PieceStorage struct {
+	basePath    string
+	completion  storage.PieceCompletion
+	dirInfohash kv.Store[string, infohash.T]
+}
+
+func NewPieceStorage(path string, completion storage.PieceCompletion) *PieceStorage {
+	return &PieceStorage{
+		basePath:   path,
+		completion: completion,
+	}
+}
+
+var _ DataStorage = (*PieceStorage)(nil)
+
+// OpenTorrent implements FileStorageDeleter.
+func (p *PieceStorage) OpenTorrent(info *metainfo.Info, infoHash infohash.T) (storage.TorrentImpl, error) {
+	torrentPath := path.Join(p.basePath, infoHash.HexString())
+	descriptors := []*os.File{}
+
+	return storage.TorrentImpl{
+		Piece: func(piece metainfo.Piece) storage.PieceImpl {
+			hash := piece.Hash().HexString()
+			piecePrefixDir := path.Join(torrentPath, hash[:2])
+			err := os.MkdirAll(piecePrefixDir, os.ModePerm|os.ModeDir)
+			if err != nil {
+				return &errPiece{err: err}
+			}
+			piecePath := path.Join(torrentPath, hash[:2], hash)
+			file, err := os.OpenFile(piecePath, os.O_CREATE|os.O_RDWR, os.ModePerm)
+			if err != nil {
+				return &errPiece{err: err}
+			}
+			pk := metainfo.PieceKey{
+				InfoHash: infoHash,
+				Index:    piece.Index(),
+			}
+			return newPieceFile(pk, file, p.completion)
+
+			// file, err os.OpenFile(piecePath)
+		},
+		Flush: func() error {
+			var res error
+			for _, f := range descriptors {
+				if err := f.Sync(); err != nil {
+					res = multierror.Append(res, err)
+				}
+			}
+			return res
+		},
+		Close: func() error {
+			var res error
+			for _, f := range descriptors {
+				if err := f.Close(); err != nil {
+					res = multierror.Append(res, err)
+				}
+			}
+			return res
+		},
+	}, nil
+}
+
+// Close implements FileStorageDeleter.
+func (p *PieceStorage) Close() error {
+	return nil
+}
+
+// DeleteFile implements FileStorageDeleter.
+func (p *PieceStorage) DeleteFile(file *torrent.File) error {
+	return fmt.Errorf("not implemented")
+}
+
+// CleanupDirs implements DataStorage.
+func (p *PieceStorage) CleanupDirs(ctx context.Context, expected []*controller.Torrent, 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) {
+	return 0, nil // TODO
+}
+
+func newPieceFile(pk metainfo.PieceKey, file *os.File, completion storage.PieceCompletionGetSetter) *piece {
+	return &piece{
+		pk:         pk,
+		File:       file,
+		completion: completion,
+	}
+}
+
+type piece struct {
+	*os.File
+	pk         metainfo.PieceKey
+	completion storage.PieceCompletionGetSetter
+}
+
+// Completion implements storage.PieceImpl.
+func (p *piece) Completion() storage.Completion {
+	compl, err := p.completion.Get(p.pk)
+	if err != nil {
+		return storage.Completion{Complete: false, Ok: false, Err: err}
+	}
+	return compl
+}
+
+// MarkComplete implements storage.PieceImpl.
+func (p *piece) MarkComplete() error {
+	return p.completion.Set(p.pk, true)
+}
+
+// MarkNotComplete implements storage.PieceImpl.
+func (p *piece) MarkNotComplete() error {
+	return p.completion.Set(p.pk, false)
+}
+
+var _ storage.PieceImpl = (*piece)(nil)
+var _ io.WriterTo = (*piece)(nil)
+
+type errPiece struct {
+	err error
+}
+
+// WriteTo implements io.WriterTo.
+func (p *errPiece) WriteTo(io.Writer) (int64, error) {
+	return 0, p.err
+}
+
+// ReadAt implements storage.PieceImpl.
+func (p *errPiece) ReadAt([]byte, int64) (int, error) {
+	return 0, p.err
+}
+
+// WriteAt implements storage.PieceImpl.
+func (p *errPiece) WriteAt([]byte, int64) (int, error) {
+	return 0, p.err
+}
+
+// Completion implements storage.PieceImpl.
+func (p *errPiece) Completion() storage.Completion {
+	return storage.Completion{Complete: false, Ok: false, Err: p.err}
+}
+
+// MarkComplete implements storage.PieceImpl.
+func (p *errPiece) MarkComplete() error {
+	return p.err
+}
+
+// MarkNotComplete implements storage.PieceImpl.
+func (p *errPiece) MarkNotComplete() error {
+	return p.err
+}
+
+var _ storage.PieceImpl = (*errPiece)(nil)
+var _ io.WriterTo = (*errPiece)(nil)
diff --git a/src/host/filestorage/setup.go b/src/host/datastorage/setup.go
similarity index 93%
rename from src/host/filestorage/setup.go
rename to src/host/datastorage/setup.go
index 17e5448..5209385 100644
--- a/src/host/filestorage/setup.go
+++ b/src/host/datastorage/setup.go
@@ -1,4 +1,4 @@
-package filestorage
+package datastorage
 
 import (
 	"fmt"
@@ -10,7 +10,7 @@ import (
 	"github.com/anacrolix/torrent/storage"
 )
 
-func Setup(cfg config.TorrentClient) (*FileStorage, storage.PieceCompletion, error) {
+func Setup(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)
diff --git a/src/host/filestorage/storage.go b/src/host/datastorage/storage.go
similarity index 94%
rename from src/host/filestorage/storage.go
rename to src/host/datastorage/storage.go
index cc933e2..c17768e 100644
--- a/src/host/filestorage/storage.go
+++ b/src/host/datastorage/storage.go
@@ -1,4 +1,4 @@
-package filestorage
+package datastorage
 
 import (
 	"context"
@@ -14,9 +14,11 @@ import (
 	"github.com/anacrolix/torrent/storage"
 )
 
-type FileStorageDeleter interface {
+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)
 }
 
 // NewFileStorage creates a new ClientImplCloser that stores files using the OS native filesystem.
@@ -137,7 +139,7 @@ func (fs *FileStorage) CleanupFiles(ctx context.Context, expected []*controller.
 	expectedEntries := []string{}
 	{
 		for _, e := range expected {
-			files, err := e.Files()
+			files, err := e.Files(ctx)
 			if err != nil {
 				return 0, err
 			}
diff --git a/src/host/service/queue.go b/src/host/service/queue.go
new file mode 100644
index 0000000..a67f286
--- /dev/null
+++ b/src/host/service/queue.go
@@ -0,0 +1,130 @@
+package service
+
+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 {
+	ID       uuid.UUID
+	InfoHash infohash.T
+	File     string
+}
+
+func (s *Service) Download(ctx context.Context, task *TorrentDownloadTask) error {
+	t, ok := s.c.Torrent(task.InfoHash)
+	if !ok {
+		return fmt.Errorf("torrent with IH %s not found", task.InfoHash.HexString())
+	}
+
+	if task.File != "" {
+		var file *torrent.File
+		for _, tf := range t.Files() {
+			if tf.Path() == task.File {
+				file = tf
+				break
+			}
+		}
+
+		if file == nil {
+			return fmt.Errorf("file %s not found in torrent torrent with IH %s", task.File, task.InfoHash.HexString())
+		}
+
+		file.Download()
+		return nil
+	}
+
+	t.DownloadAll()
+	return nil
+}
+
+// func (s *Service) DownloadAndWait(ctx context.Context, task *TorrentDownloadTask) error {
+// 	t, ok := s.c.Torrent(task.InfoHash)
+// 	if !ok {
+// 		return fmt.Errorf("torrent with IH %s not found", task.InfoHash.HexString())
+// 	}
+
+// 	if task.File != "" {
+// 		var file *torrent.File
+// 		for _, tf := range t.Files() {
+// 			if tf.Path() == task.File {
+// 				file = tf
+// 				break
+// 			}
+// 		}
+
+// 		if file == nil {
+// 			return fmt.Errorf("file %s not found in torrent torrent with IH %s", task.File, task.InfoHash.HexString())
+// 		}
+
+// 		file.Download()
+// 		return waitPieceRange(ctx, t, file.BeginPieceIndex(), file.EndPieceIndex())
+
+// 	}
+
+// 	t.DownloadAll()
+// 	select {
+// 	case <-ctx.Done():
+// 		return ctx.Err()
+// 	case <-t.Complete.On():
+// 		return nil
+// 	}
+// }
+
+// func waitPieceRange(ctx context.Context, t *torrent.Torrent, start, end int) error {
+// 	for i := start; i < end; i++ {
+// 		timer := time.NewTimer(time.Millisecond)
+// 		for {
+// 			select {
+// 			case <-ctx.Done():
+// 				return ctx.Err()
+// 			case <-timer.C:
+// 				if t.PieceState(i).Complete {
+// 					continue
+// 				}
+// 			}
+
+// 		}
+// 	}
+// 	return nil
+// }
+
+type TorrentProgress struct {
+	Torrent *controller.Torrent
+	Current int64
+	Total   int64
+}
+
+func (s *Service) DownloadProgress(ctx context.Context) (<-chan TorrentProgress, error) {
+	torrents, err := s.ListTorrents(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	out := make(chan TorrentProgress, 1)
+	go func() {
+		defer close(out)
+		for _, t := range torrents {
+			sub := t.Torrent().SubscribePieceStateChanges()
+			go func() {
+				for range sub.Values {
+					out <- TorrentProgress{
+						Torrent: t,
+						Current: t.BytesCompleted(),
+						Total:   t.Length(),
+					}
+				}
+			}()
+			defer sub.Close()
+		}
+
+		<-ctx.Done()
+	}()
+
+	return out, nil
+}
diff --git a/src/host/service/service.go b/src/host/service/service.go
index 718e30e..4388058 100644
--- a/src/host/service/service.go
+++ b/src/host/service/service.go
@@ -11,9 +11,10 @@ import (
 	"time"
 
 	"git.kmsign.ru/royalcat/tstor/src/host/controller"
-	"git.kmsign.ru/royalcat/tstor/src/host/filestorage"
+	"git.kmsign.ru/royalcat/tstor/src/host/datastorage"
 	"git.kmsign.ru/royalcat/tstor/src/host/store"
 	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
+	"go.uber.org/multierr"
 
 	"github.com/anacrolix/torrent"
 	"github.com/anacrolix/torrent/bencode"
@@ -24,21 +25,21 @@ import (
 
 type Service struct {
 	c             *torrent.Client
-	excludedFiles *store.ExlcudedFiles
+	excludedFiles *store.FilesMappings
 	infoBytes     *store.InfoBytes
 
 	torrentLoaded chan struct{}
 
 	// stats *Stats
 	DefaultPriority types.PiecePriority
-	Storage         *filestorage.FileStorage
+	Storage         datastorage.DataStorage
 	SourceDir       string
 
 	log                     *slog.Logger
 	addTimeout, readTimeout int
 }
 
-func NewService(sourceDir string, c *torrent.Client, storage *filestorage.FileStorage, excludedFiles *store.ExlcudedFiles, infoBytes *store.InfoBytes, addTimeout, readTimeout int) *Service {
+func NewService(sourceDir string, c *torrent.Client, storage datastorage.DataStorage, excludedFiles *store.FilesMappings, infoBytes *store.InfoBytes, addTimeout, readTimeout int) *Service {
 	s := &Service{
 		log:             slog.With("component", "torrent-service"),
 		c:               c,
@@ -66,6 +67,12 @@ func NewService(sourceDir string, c *torrent.Client, storage *filestorage.FileSt
 
 var _ vfs.FsFactory = (*Service)(nil).NewTorrentFs
 
+func (s *Service) Close() error {
+	err := multierr.Combine(s.c.Close()...)
+	err = multierr.Append(err, s.Storage.Close())
+	return err
+}
+
 func (s *Service) AddTorrent(ctx context.Context, f vfs.File) (*torrent.Torrent, error) {
 	defer f.Close()
 
@@ -102,17 +109,17 @@ func (s *Service) AddTorrent(ctx context.Context, f vfs.File) (*torrent.Torrent,
 		if err != nil {
 			infoBytes = nil
 		} else {
-			for _, t := range s.c.Torrents() {
-				if t.Name() == info.BestName() && t.InfoHash() != spec.InfoHash {
-					<-t.GotInfo()
-					if !isTorrentCompatable(*t.Info(), info) {
-						return nil, fmt.Errorf(
-							"torrent with name '%s' not compatable existing infohash: %s, new: %s",
-							t.Name(), t.InfoHash().HexString(), spec.InfoHash.HexString(),
-						)
-					}
-				}
-			}
+			// for _, t := range s.c.Torrents() {
+			// 	if t.Name() == info.BestName() && t.InfoHash() != spec.InfoHash {
+			// 		<-t.GotInfo()
+			// 		if !isTorrentCompatable(*t.Info(), info) {
+			// 			return nil, fmt.Errorf(
+			// 				"torrent with name '%s' not compatable existing infohash: %s, new: %s",
+			// 				t.Name(), t.InfoHash().HexString(), spec.InfoHash.HexString(),
+			// 			)
+			// 		}
+			// 	}
+			// }
 		}
 
 		t, _ = s.c.AddTorrentOpt(torrent.AddTorrentOpts{
@@ -123,7 +130,6 @@ func (s *Service) AddTorrent(ctx context.Context, f vfs.File) (*torrent.Torrent,
 		})
 		t.AllowDataDownload()
 		t.AllowDataUpload()
-		t.DownloadAll()
 
 		select {
 		case <-ctx.Done():
diff --git a/src/host/store/excluded-files.go b/src/host/store/excluded-files.go
deleted file mode 100644
index cdcd585..0000000
--- a/src/host/store/excluded-files.go
+++ /dev/null
@@ -1,94 +0,0 @@
-package store
-
-import (
-	"errors"
-	"path/filepath"
-	"sync"
-
-	"github.com/anacrolix/torrent"
-	"github.com/anacrolix/torrent/metainfo"
-	"github.com/philippgille/gokv"
-	"github.com/philippgille/gokv/badgerdb"
-	"github.com/philippgille/gokv/encoding"
-)
-
-func NewExcludedFiles(metaDir string, storage TorrentFileDeleter) (*ExlcudedFiles, error) {
-	excludedFilesStore, err := badgerdb.NewStore(badgerdb.Options{
-		Dir:   filepath.Join(metaDir, "excluded-files"),
-		Codec: encoding.JSON,
-	})
-
-	if err != nil {
-		return nil, err
-	}
-
-	r := &ExlcudedFiles{
-		excludedFiles: excludedFilesStore,
-		storage:       storage,
-	}
-
-	return r, nil
-}
-
-type ExlcudedFiles struct {
-	m             sync.RWMutex
-	excludedFiles gokv.Store
-	storage       TorrentFileDeleter
-}
-
-var ErrNotFound = errors.New("not found")
-
-type TorrentFileDeleter interface {
-	DeleteFile(file *torrent.File) error
-}
-
-func (r *ExlcudedFiles) ExcludeFile(file *torrent.File) error {
-	r.m.Lock()
-	defer r.m.Unlock()
-
-	hash := file.Torrent().InfoHash()
-	var excludedFiles []string
-	found, err := r.excludedFiles.Get(hash.AsString(), &excludedFiles)
-	if err != nil {
-		return err
-	}
-	if !found {
-		excludedFiles = []string{}
-	}
-	excludedFiles = unique(append(excludedFiles, file.Path()))
-
-	err = r.storage.DeleteFile(file)
-	if err != nil {
-		return err
-	}
-
-	return r.excludedFiles.Set(hash.AsString(), excludedFiles)
-}
-
-func (r *ExlcudedFiles) ExcludedFiles(hash metainfo.Hash) ([]string, error) {
-	r.m.Lock()
-	defer r.m.Unlock()
-
-	var excludedFiles []string
-	found, err := r.excludedFiles.Get(hash.AsString(), &excludedFiles)
-	if err != nil {
-		return nil, err
-	}
-	if !found {
-		return nil, nil
-	}
-
-	return excludedFiles, nil
-}
-
-func unique[C comparable](intSlice []C) []C {
-	keys := make(map[C]bool)
-	list := []C{}
-	for _, entry := range intSlice {
-		if _, value := keys[entry]; !value {
-			keys[entry] = true
-			list = append(list, entry)
-		}
-	}
-	return list
-}
diff --git a/src/host/store/file-mappings.go b/src/host/store/file-mappings.go
new file mode 100644
index 0000000..2a1c5b2
--- /dev/null
+++ b/src/host/store/file-mappings.go
@@ -0,0 +1,57 @@
+package store
+
+import (
+	"context"
+	"errors"
+	"path/filepath"
+
+	"github.com/anacrolix/torrent"
+	"github.com/anacrolix/torrent/types/infohash"
+	"github.com/royalcat/kv"
+)
+
+func NewFileMappings(metaDir string, storage TorrentFileDeleter) (*FilesMappings, error) {
+	str, err := kv.NewBadgerKVBytes[string, string](filepath.Join(metaDir, "file-mappings"))
+	if err != nil {
+		return nil, err
+	}
+
+	r := &FilesMappings{
+		mappings: str,
+		storage:  storage,
+	}
+
+	return r, nil
+}
+
+type FilesMappings struct {
+	mappings kv.Store[string, string]
+	storage  TorrentFileDeleter
+}
+
+var ErrNotFound = errors.New("not found")
+
+type TorrentFileDeleter interface {
+	DeleteFile(file *torrent.File) error
+}
+
+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 {
+	return r.mappings.Set(ctx, fileKey(file), target)
+}
+
+func (r *FilesMappings) 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) {
+	out := map[string]string{}
+	err := r.mappings.RangeWithPrefix(ctx, ih.HexString(), func(k, v string) bool {
+		out[k] = v
+		return true
+	})
+	return out, err
+}
diff --git a/src/host/store/store.go b/src/host/store/fileitem.go
similarity index 100%
rename from src/host/store/store.go
rename to src/host/store/fileitem.go
diff --git a/src/host/vfs/fs_test.go b/src/host/vfs/fs_test.go
index 414f556..3caef23 100644
--- a/src/host/vfs/fs_test.go
+++ b/src/host/vfs/fs_test.go
@@ -21,7 +21,7 @@ func TestFileinfo(t *testing.T) {
 	require.Zero(fi.Type() & fs.ModeDir)
 	require.Zero(fi.Mode() & fs.ModeDir)
 	require.Equal(fs.FileMode(0555), fi.Mode())
-	require.Equal(nil, fi.Sys())
+	require.Nil(fi.Sys())
 }
 
 func TestDirInfo(t *testing.T) {
@@ -38,6 +38,6 @@ func TestDirInfo(t *testing.T) {
 	require.NotZero(fi.Type() & fs.ModeDir)
 	require.NotZero(fi.Mode() & fs.ModeDir)
 	require.Equal(defaultMode|fs.ModeDir, fi.Mode())
-	require.Equal(nil, fi.Sys())
+	require.Nil(fi.Sys())
 
 }
diff --git a/src/host/vfs/torrent.go b/src/host/vfs/torrent.go
index 6d3069e..0bbe3b6 100644
--- a/src/host/vfs/torrent.go
+++ b/src/host/vfs/torrent.go
@@ -12,6 +12,7 @@ import (
 
 	"git.kmsign.ru/royalcat/tstor/src/host/controller"
 	"git.kmsign.ru/royalcat/tstor/src/iio"
+	"github.com/RoaringBitmap/roaring"
 	"github.com/anacrolix/missinggo/v2"
 	"github.com/anacrolix/torrent"
 	"golang.org/x/exp/maps"
@@ -46,17 +47,14 @@ func (fs *TorrentFs) files() (map[string]File, error) {
 		return fs.filesCache, nil
 	}
 
-	files, err := fs.c.Files()
+	files, err := fs.c.Files(context.Background())
 	if err != nil {
 		return nil, err
 	}
 
 	fs.filesCache = make(map[string]File)
 	for _, file := range files {
-		if file.BytesCompleted() == 0 {
-			continue
-		}
-
+		file.Download()
 		p := AbsPath(file.Path())
 
 		fs.filesCache[p] = &torrentFile{
@@ -107,6 +105,24 @@ 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 listFilesRecursive(vfs Filesystem, start string) (map[string]File, error) {
 	out := make(map[string]File, 0)
 	entries, err := vfs.ReadDir(start)
@@ -222,7 +238,7 @@ func (fs *TorrentFs) Unlink(name string) error {
 		return ErrNotImplemented
 	}
 
-	return fs.c.ExcludeFile(tfile.file)
+	return fs.c.ExcludeFile(context.Background(), tfile.file)
 }
 
 type reader interface {
diff --git a/src/host/vfs/virtdir/vds.go b/src/host/vfs/virtdir/vds.go
new file mode 100644
index 0000000..31ccd60
--- /dev/null
+++ b/src/host/vfs/virtdir/vds.go
@@ -0,0 +1,21 @@
+package virtdir
+
+type SourceType string
+
+const (
+	VirtDirYtDlp SourceType = "yt-dlp"
+)
+
+type VirtDirSource interface {
+	SourceType() SourceType
+}
+
+var _ VirtDirSource = (*VirtDirSourceYtDlp)(nil)
+
+type VirtDirSourceYtDlp struct {
+	URL string `json:"url"`
+}
+
+func (VirtDirSourceYtDlp) SourceType() SourceType {
+	return VirtDirYtDlp
+}
diff --git a/src/log/nfs.go b/src/log/nfs.go
index cde0632..b48b46c 100644
--- a/src/log/nfs.go
+++ b/src/log/nfs.go
@@ -11,13 +11,14 @@ import (
 var _ nfs.Logger = (*NFSLog)(nil)
 
 type NFSLog struct {
+	level nfs.LogLevel
 	// r *slog.Logger
 	l *slog.Logger
 }
 
 func NewNFSLog(r *slog.Logger) nfs.Logger {
 	return &NFSLog{
-		// r: r,
+		level: nfs.DebugLevel,
 		// l: r.Level(zerolog.DebugLevel),
 		l: r,
 	}
@@ -25,43 +26,75 @@ func NewNFSLog(r *slog.Logger) nfs.Logger {
 
 // Debug implements nfs.Logger.
 func (l *NFSLog) Debug(args ...interface{}) {
+	if l.level < nfs.DebugLevel {
+		return
+	}
+
 	l.l.Debug(fmt.Sprint(args...))
 }
 
 // Debugf implements nfs.Logger.
 func (l *NFSLog) Debugf(format string, args ...interface{}) {
+	if l.level < nfs.DebugLevel {
+		return
+	}
+
 	l.l.Debug(fmt.Sprintf(format, args...))
 }
 
 // Error implements nfs.Logger.
 func (l *NFSLog) Error(args ...interface{}) {
+	if l.level < nfs.ErrorLevel {
+		return
+	}
+
 	l.l.Error(fmt.Sprint(args...))
 }
 
 // Errorf implements nfs.Logger.
 func (l *NFSLog) Errorf(format string, args ...interface{}) {
+	if l.level < nfs.ErrorLevel {
+		return
+	}
+
 	l.l.Error(fmt.Sprintf(format, args...))
 }
 
 // Fatal implements nfs.Logger.
 func (l *NFSLog) Fatal(args ...interface{}) {
+	if l.level < nfs.FatalLevel {
+		return
+	}
+
 	l.l.Error(fmt.Sprint(args...))
 	log.Fatal(args...)
 }
 
 // Fatalf implements nfs.Logger.
 func (l *NFSLog) Fatalf(format string, args ...interface{}) {
+	if l.level < nfs.FatalLevel {
+		return
+	}
+
 	l.l.Error(fmt.Sprintf(format, args...))
 	log.Fatalf(format, args...)
 }
 
 // Info implements nfs.Logger.
 func (l *NFSLog) Info(args ...interface{}) {
+	if l.level < nfs.InfoLevel {
+		return
+	}
+
 	l.l.Info(fmt.Sprint(args...))
 }
 
 // Infof implements nfs.Logger.
 func (l *NFSLog) Infof(format string, args ...interface{}) {
+	if l.level < nfs.InfoLevel {
+		return
+	}
+
 	l.l.Info(fmt.Sprintf(format, args...))
 }
 
@@ -79,102 +112,85 @@ func (l *NFSLog) Panicf(format string, args ...interface{}) {
 
 // Print implements nfs.Logger.
 func (l *NFSLog) Print(args ...interface{}) {
+	if l.level < nfs.InfoLevel {
+		return
+	}
+
 	l.l.Info(fmt.Sprint(args...))
 }
 
 // Printf implements nfs.Logger.
 func (l *NFSLog) Printf(format string, args ...interface{}) {
+	if l.level < nfs.InfoLevel {
+		return
+	}
+
 	l.l.Info(fmt.Sprintf(format, args...))
 }
 
 // Trace implements nfs.Logger.
 func (l *NFSLog) Trace(args ...interface{}) {
+	if l.level < nfs.TraceLevel {
+		return
+	}
+
 	l.l.Debug(fmt.Sprint(args...))
 }
 
 // Tracef implements nfs.Logger.
 func (l *NFSLog) Tracef(format string, args ...interface{}) {
+	if l.level < nfs.TraceLevel {
+		return
+	}
+
 	l.l.Debug(fmt.Sprintf(format, args...))
 }
 
 // Warn implements nfs.Logger.
 func (l *NFSLog) Warn(args ...interface{}) {
+	if l.level < nfs.WarnLevel {
+		return
+	}
+
 	l.l.Warn(fmt.Sprint(args...))
 }
 
 // Warnf implements nfs.Logger.
 func (l *NFSLog) Warnf(format string, args ...interface{}) {
+	if l.level < nfs.WarnLevel {
+		return
+	}
+
 	l.l.Warn(fmt.Sprintf(format, args...))
 }
 
 // GetLevel implements nfs.Logger.
 func (l *NFSLog) GetLevel() nfs.LogLevel {
-	// zl := l.l.Handler()
-	// switch zl {
-	// case zerolog.PanicLevel, zerolog.Disabled:
-	// 	return nfs.PanicLevel
-	// case zerolog.FatalLevel:
-	// 	return nfs.FatalLevel
-	// case zerolog.ErrorLevel:
-	// 	return nfs.ErrorLevel
-	// case zerolog.WarnLevel:
-	// 	return nfs.WarnLevel
-	// case zerolog.InfoLevel:
-	// 	return nfs.InfoLevel
-	// case zerolog.DebugLevel:
-	// 	return nfs.DebugLevel
-	// case zerolog.TraceLevel:
-	// 	return nfs.TraceLevel
-	// }
-	return nfs.TraceLevel
+	return l.level
 }
 
 // ParseLevel implements nfs.Logger.
 func (l *NFSLog) ParseLevel(level string) (nfs.LogLevel, error) {
-	// switch level {
-	// case "panic":
-	// 	return nfs.PanicLevel, nil
-	// case "fatal":
-	// 	return nfs.FatalLevel, nil
-	// case "error":
-	// 	return nfs.ErrorLevel, nil
-	// case "warn":
-	// 	return nfs.WarnLevel, nil
-	// case "info":
-	// 	return nfs.InfoLevel, nil
-	// case "debug":
-	// 	return nfs.DebugLevel, nil
-	// case "trace":
-	// 	return nfs.TraceLevel, nil
-	// }
-	// var ll nfs.LogLevel
-	// return ll, fmt.Errorf("invalid log level %q", level)
-	return nfs.TraceLevel, fmt.Errorf("level change not supported")
+	switch level {
+	case "panic":
+		return nfs.PanicLevel, nil
+	case "fatal":
+		return nfs.FatalLevel, nil
+	case "error":
+		return nfs.ErrorLevel, nil
+	case "warn":
+		return nfs.WarnLevel, nil
+	case "info":
+		return nfs.InfoLevel, nil
+	case "debug":
+		return nfs.DebugLevel, nil
+	case "trace":
+		return nfs.TraceLevel, nil
+	}
+	return 0, fmt.Errorf("invalid log level %q", level)
 }
 
 // SetLevel implements nfs.Logger.
 func (l *NFSLog) SetLevel(level nfs.LogLevel) {
-	// switch level {
-	// case nfs.PanicLevel:
-	// 	l.l = l.r.Level(zerolog.PanicLevel)
-	// 	return
-	// case nfs.FatalLevel:
-	// 	l.l = l.r.Level(zerolog.FatalLevel)
-	// 	return
-	// case nfs.ErrorLevel:
-	// 	l.l = l.r.Level(zerolog.ErrorLevel)
-	// 	return
-	// case nfs.WarnLevel:
-	// 	l.l = l.r.Level(zerolog.WarnLevel)
-	// 	return
-	// case nfs.InfoLevel:
-	// 	l.l = l.r.Level(zerolog.InfoLevel)
-	// 	return
-	// case nfs.DebugLevel:
-	// 	l.l = l.r.Level(zerolog.DebugLevel)
-	// 	return
-	// case nfs.TraceLevel:
-	// 	l.l = l.r.Level(zerolog.TraceLevel)
-	// 	return
-	// }
+	l.level = level
 }

From e576e625994fdbc8a2398f2412a059fe675c6069 Mon Sep 17 00:00:00 2001
From: royalcat <k.adamovich20@gmail.com>
Date: Wed, 20 Mar 2024 00:30:37 +0300
Subject: [PATCH 14/18] gql dir ls

---
 cmd/tstor/main.go                             |  27 +-
 go.mod                                        |  46 +-
 go.sum                                        | 108 +-
 graphql/query.graphql                         |   3 +-
 graphql/types/fs.graphql                      |  26 +
 pkg/rlog/rlog.go                              |  70 ++
 src/config/model.go                           |   2 +
 src/{http => delivery}/api.go                 |   2 +-
 src/delivery/graphql/generated.go             | 999 +++++++++++++++++-
 src/delivery/graphql/model/models_gen.go      |  43 +
 .../graphql/resolver/query.resolvers.go       |  37 +
 src/delivery/graphql/resolver/resolver.go     |   6 +-
 src/{http => delivery}/http.go                |  10 +-
 src/{http => delivery}/model.go               |   2 +-
 src/delivery/router.go                        |   8 +-
 src/{http => delivery}/web.go                 |   2 +-
 src/host/service/service.go                   | 176 ++-
 src/host/vfs/archive.go                       |  16 +-
 src/host/vfs/resolver.go                      |  16 +-
 src/host/vfs/resolver_test.go                 |   6 +-
 src/host/vfs/torrent.go                       |  43 +-
 src/log/log.go                                |  35 +-
 src/telemetry/setup.go                        | 126 +++
 23 files changed, 1671 insertions(+), 138 deletions(-)
 create mode 100644 graphql/types/fs.graphql
 create mode 100644 pkg/rlog/rlog.go
 rename src/{http => delivery}/api.go (99%)
 rename src/{http => delivery}/http.go (91%)
 rename src/{http => delivery}/model.go (88%)
 rename src/{http => delivery}/web.go (96%)
 create mode 100644 src/telemetry/setup.go

diff --git a/cmd/tstor/main.go b/cmd/tstor/main.go
index 3b4dce0..94979f5 100644
--- a/cmd/tstor/main.go
+++ b/cmd/tstor/main.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"context"
 	"fmt"
 	"log/slog"
 
@@ -14,20 +15,21 @@ import (
 	"time"
 
 	"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/datastorage"
 	"git.kmsign.ru/royalcat/tstor/src/host/service"
 	"git.kmsign.ru/royalcat/tstor/src/host/store"
 	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
+	"git.kmsign.ru/royalcat/tstor/src/telemetry"
 	"github.com/urfave/cli/v2"
 	wnfs "github.com/willscott/go-nfs"
 
+	_ "git.kmsign.ru/royalcat/tstor/pkg/rlog"
 	"git.kmsign.ru/royalcat/tstor/src/export/fuse"
 	"git.kmsign.ru/royalcat/tstor/src/export/httpfs"
 	"git.kmsign.ru/royalcat/tstor/src/export/nfs"
 	"git.kmsign.ru/royalcat/tstor/src/export/webdav"
-	"git.kmsign.ru/royalcat/tstor/src/http"
-	dlog "git.kmsign.ru/royalcat/tstor/src/log"
 )
 
 const (
@@ -65,7 +67,17 @@ func run(configPath string) error {
 	if err != nil {
 		return fmt.Errorf("error loading configuration: %w", err)
 	}
-	dlog.Load(&conf.Log)
+	// dlog.Load(&conf.Log)
+
+	if conf.OtelHttp != "" {
+		ctx := context.Background()
+		client, err := telemetry.Setup(ctx, conf.OtelHttp)
+		if err != nil {
+			return err
+		}
+
+		defer client.Shutdown(ctx)
+	}
 
 	log := slog.Default().With("component", "run")
 
@@ -113,7 +125,10 @@ func run(configPath string) error {
 	c.AddDhtNodes(conf.TorrentClient.DHTNodes)
 	defer c.Close()
 
-	ts := service.NewService(conf.SourceDir, c, st, excludedFilesStore, infoBytesStore, conf.TorrentClient.AddTimeout, conf.TorrentClient.ReadTimeout)
+	ts, err := service.NewService(conf.SourceDir, conf.TorrentClient, c, st, excludedFilesStore, infoBytesStore, conf.TorrentClient.AddTimeout, conf.TorrentClient.ReadTimeout)
+	if err != nil {
+		return fmt.Errorf("error creating service: %w", err)
+	}
 
 	if err := os.MkdirAll(conf.SourceDir, 0744); err != nil {
 		return fmt.Errorf("error creating data folder: %w", err)
@@ -226,9 +241,9 @@ func run(configPath string) error {
 	}()
 
 	go func() {
-		logFilename := filepath.Join(conf.Log.Path, dlog.FileName)
+		logFilename := filepath.Join(conf.Log.Path, "logs")
 
-		err := http.New(nil, service.NewStats(), ts, logFilename, conf)
+		err := delivery.New(nil, service.NewStats(), ts, sfs, logFilename, conf)
 		if err != nil {
 			log.Error("error initializing HTTP server", "error", err)
 		}
diff --git a/go.mod b/go.mod
index 047396f..4d9c47f 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,9 @@ go 1.22.1
 require (
 	github.com/99designs/gqlgen v0.17.43
 	github.com/RoaringBitmap/roaring v1.2.3
-	github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444
+	github.com/agoda-com/opentelemetry-go/otelslog v0.1.1
+	github.com/agoda-com/opentelemetry-logs-go v0.3.0
+	github.com/anacrolix/dht/v2 v2.21.1
 	github.com/anacrolix/log v0.14.6-0.20231202035202-ed7a02cad0b4
 	github.com/anacrolix/missinggo/v2 v2.7.3
 	github.com/anacrolix/torrent v1.55.0
@@ -25,14 +27,22 @@ require (
 	github.com/knadh/koanf/providers/file v0.1.0
 	github.com/knadh/koanf/providers/structs v0.1.0
 	github.com/knadh/koanf/v2 v2.0.1
-	github.com/lmittmann/tint v1.0.4
 	github.com/nwaples/rardecode/v2 v2.0.0-beta.2
-	github.com/royalcat/kv v0.0.0-20240316134516-1bb692abce73
+	github.com/ravilushqa/otelgqlgen v0.15.0
+	github.com/royalcat/kv v0.0.0-20240318203654-181645f85b10
+	github.com/rs/zerolog v1.32.0
+	github.com/samber/slog-multi v1.0.2
+	github.com/samber/slog-zerolog v1.0.0
 	github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c
 	github.com/stretchr/testify v1.8.4
 	github.com/urfave/cli/v2 v2.27.0
 	github.com/vektah/gqlparser/v2 v2.5.11
 	github.com/willscott/go-nfs v0.0.2
+	go.opentelemetry.io/otel v1.24.0
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0
+	go.opentelemetry.io/otel/exporters/prometheus v0.46.0
+	go.opentelemetry.io/otel/sdk v1.24.0
+	go.opentelemetry.io/otel/sdk/metric v1.24.0
 	go.uber.org/multierr v1.11.0
 	golang.org/x/exp v0.0.0-20231226003508-02704c960a9b
 	golang.org/x/net v0.19.0
@@ -57,11 +67,13 @@ require (
 	github.com/andybalholm/brotli v1.0.6 // indirect
 	github.com/bahlo/generic-list-go v0.2.0 // indirect
 	github.com/benbjohnson/immutable v0.4.1-0.20221220213129-8932b999621d // indirect
+	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/bits-and-blooms/bitset v1.2.2 // indirect
 	github.com/bodgit/plumbing v1.3.0 // indirect
 	github.com/bodgit/windows v1.0.1 // indirect
 	github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
 	github.com/bytedance/sonic v1.9.1 // indirect
+	github.com/cenkalti/backoff/v4 v4.2.1 // indirect
 	github.com/cespare/xxhash v1.1.0 // indirect
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 	github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
@@ -75,20 +87,21 @@ require (
 	github.com/gin-contrib/sse v0.1.0 // indirect
 	github.com/go-llsqlite/adapter v0.0.0-20230927005056-7f5ce7f0c916 // indirect
 	github.com/go-llsqlite/crawshaw v0.4.0 // indirect
-	github.com/go-logr/logr v1.2.4 // indirect
+	github.com/go-logr/logr v1.4.1 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-playground/locales v0.14.1 // indirect
 	github.com/go-playground/universal-translator v0.18.1 // indirect
 	github.com/go-playground/validator/v10 v10.14.0 // indirect
 	github.com/goccy/go-json v0.10.2 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
-	github.com/golang/glog v1.1.0 // indirect
+	github.com/golang/glog v1.1.2 // indirect
 	github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
 	github.com/golang/protobuf v1.5.3 // indirect
 	github.com/golang/snappy v0.0.4 // indirect
 	github.com/google/btree v1.1.2 // indirect
 	github.com/google/flatbuffers v2.0.8+incompatible // indirect
 	github.com/gorilla/websocket v1.5.0 // indirect
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect
 	github.com/hashicorp/errwrap v1.0.0 // indirect
 	github.com/huandu/xstrings v1.3.2 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
@@ -96,7 +109,9 @@ require (
 	github.com/klauspost/cpuid/v2 v2.2.4 // indirect
 	github.com/knadh/koanf/maps v0.1.1 // indirect
 	github.com/leodido/go-urn v1.2.4 // indirect
+	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
 	github.com/mitchellh/copystructure v1.2.0 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/mitchellh/reflectwalk v1.0.2 // indirect
@@ -125,12 +140,17 @@ require (
 	github.com/pion/webrtc/v3 v3.1.42 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/prometheus/client_golang v1.18.0 // indirect
+	github.com/prometheus/client_model v0.6.0 // indirect
+	github.com/prometheus/common v0.45.0 // indirect
+	github.com/prometheus/procfs v0.12.0 // indirect
 	github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93 // indirect
 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
 	github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect
-	github.com/sosodev/duration v1.1.0 // indirect
+	github.com/samber/lo v1.38.1 // indirect
+	github.com/sosodev/duration v1.2.0 // indirect
 	github.com/tidwall/btree v1.6.0 // indirect
 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
 	github.com/ugorji/go/codec v1.2.11 // indirect
@@ -139,18 +159,24 @@ require (
 	github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
 	go.etcd.io/bbolt v1.3.6 // indirect
 	go.opencensus.io v0.24.0 // indirect
-	go.opentelemetry.io/otel v1.8.0 // indirect
-	go.opentelemetry.io/otel/trace v1.8.0 // indirect
+	go.opentelemetry.io/contrib v1.21.1 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect
+	go.opentelemetry.io/otel/metric v1.24.0 // indirect
+	go.opentelemetry.io/otel/trace v1.24.0 // indirect
+	go.opentelemetry.io/proto/otlp v1.1.0 // indirect
 	go4.org v0.0.0-20200411211856-f5505b9728dd // indirect
 	golang.org/x/arch v0.3.0 // indirect
 	golang.org/x/crypto v0.17.0 // indirect
 	golang.org/x/mod v0.14.0 // indirect
 	golang.org/x/sync v0.5.0 // indirect
-	golang.org/x/sys v0.16.0 // indirect
+	golang.org/x/sys v0.17.0 // indirect
 	golang.org/x/text v0.14.0 // indirect
 	golang.org/x/time v0.5.0 // indirect
 	golang.org/x/tools v0.16.0 // indirect
-	google.golang.org/protobuf v1.30.0 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 // indirect
+	google.golang.org/grpc v1.61.1 // indirect
+	google.golang.org/protobuf v1.32.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	modernc.org/libc v1.22.3 // indirect
 	modernc.org/mathutil v1.5.0 // indirect
diff --git a/go.sum b/go.sum
index 29df124..f56ac68 100644
--- a/go.sum
+++ b/go.sum
@@ -34,6 +34,10 @@ github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWX
 github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
 github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=
 github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
+github.com/agoda-com/opentelemetry-go/otelslog v0.1.1 h1:6nV8PZCzySHuh9kP/HZ2OJqGucwQiM+yZRugKDvtzj4=
+github.com/agoda-com/opentelemetry-go/otelslog v0.1.1/go.mod h1:CSc0veIcY/HsIfH7l5PGtIpRvBttk09QUQlweVkD2PI=
+github.com/agoda-com/opentelemetry-logs-go v0.3.0 h1:d2lMVUfCDeLzVgTxMeSU8IWaMXjwD4sVKigEZBGwcsw=
+github.com/agoda-com/opentelemetry-logs-go v0.3.0/go.mod h1:k3QR1O5AOl+dFC7pkrK9wWmoD72jjDONPFHi9dAgLJc=
 github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 h1:byYvvbfSo3+9efR4IeReh77gVs4PnNDR3AMOE9NJ7a0=
 github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0/go.mod h1:q37NoqncT41qKc048STsifIt69LfUJ8SrWWcz/yam5k=
 github.com/alecthomas/assert/v2 v2.0.0-alpha3 h1:pcHeMvQ3OMstAWgaeaXIAL8uzB9xMm2zlxt+/4ml8lk=
@@ -48,8 +52,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
 github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/anacrolix/chansync v0.3.0 h1:lRu9tbeuw3wl+PhMu/r+JJCRu5ArFXIluOgdF0ao6/U=
 github.com/anacrolix/chansync v0.3.0/go.mod h1:DZsatdsdXxD0WiwcGl0nJVwyjCKMDv+knl1q2iBjA2k=
-github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444 h1:8V0K09lrGoeT2KRJNOtspA7q+OMxGwQqK/Ug0IiaaRE=
-github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444/go.mod h1:MctKM1HS5YYDb3F30NGJxLE+QPuqWoT5ReW/4jt8xew=
+github.com/anacrolix/dht/v2 v2.21.1 h1:s1rKkfLLcmBHKv4v/mtMkIeHIEptzEFiB6xVu54+5/o=
+github.com/anacrolix/dht/v2 v2.21.1/go.mod h1:SDGC+sEs1pnO2sJGYuhvIis7T8749dDHNfcjtdH4e3g=
 github.com/anacrolix/envpprof v0.0.0-20180404065416-323002cec2fa/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
 github.com/anacrolix/envpprof v1.0.0/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c=
 github.com/anacrolix/envpprof v1.1.0/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4=
@@ -115,6 +119,7 @@ github.com/benbjohnson/immutable v0.4.1-0.20221220213129-8932b999621d h1:2qVb9bs
 github.com/benbjohnson/immutable v0.4.1-0.20221220213129-8932b999621d/go.mod h1:iAr8OjJGLnLmVUr9MZ/rz4PWUy6Ouc2JLYuMArmvAJM=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/billziss-gh/cgofuse v1.5.0 h1:kH516I/s+Ab4diL/Y/ayFeUjjA8ey+JK12xDfBf4HEs=
 github.com/billziss-gh/cgofuse v1.5.0/go.mod h1:LJjoaUojlVjgo5GQoEJTcJNqZJeRU0nCR84CyxKt2YM=
@@ -134,6 +139,8 @@ github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8/go.mod h1:spo1JLcs67
 github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
 github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
 github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
+github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
+github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
@@ -148,6 +155,7 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
 github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
 github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@@ -212,8 +220,8 @@ github.com/go-llsqlite/crawshaw v0.4.0/go.mod h1:/YJdV7uBQaYDE0fwe4z3wwJIZBJxdYz
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
-github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
+github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
@@ -233,6 +241,7 @@ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg78
 github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
 github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M=
 github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -240,8 +249,8 @@ github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE=
-github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ=
+github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo=
+github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ=
 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
@@ -284,8 +293,8 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
-github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
@@ -306,6 +315,8 @@ github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51
 github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
 github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
 github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU=
 github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
@@ -367,12 +378,16 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
 github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
 github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
-github.com/lmittmann/tint v1.0.4 h1:LeYihpJ9hyGvE0w+K2okPTGUdVLfng1+nDNVR4vWISc=
-github.com/lmittmann/tint v1.0.4/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
+github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
 github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
 github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
@@ -465,21 +480,31 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP
 github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
 github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
 github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
+github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
+github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos=
+github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
 github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
+github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
+github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
 github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
+github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
+github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
 github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93 h1:UVArwN/wkKjMVhh2EQGC0tEc1+FqiLlvYXY5mQ2f8Wg=
 github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93/go.mod h1:Nfe4efndBz4TibWycNE+lqyJZiMX4ycx+QKV8Ta0f/o=
+github.com/ravilushqa/otelgqlgen v0.15.0 h1:U85nrlweMXTGaMChUViYM39/MXBZVeVVlpuHq+6eECQ=
+github.com/ravilushqa/otelgqlgen v0.15.0/go.mod h1:o+1Eju0VySmgq2BP8Vupz2YrN21Bj7D7imBqu3m2uB8=
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
@@ -489,17 +514,24 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE
 github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
 github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
 github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
-github.com/royalcat/kv v0.0.0-20240316120131-b774a9bff6f7 h1:fmBTD0RaTvWbd6KrgLVXSDUJ4dWTPOFUdkdHp+kYvRM=
-github.com/royalcat/kv v0.0.0-20240316120131-b774a9bff6f7/go.mod h1:Ff0Z/r1H3ojacpEe8SashMKJx6YCIhWrYtpdV8Y/k3A=
-github.com/royalcat/kv v0.0.0-20240316134516-1bb692abce73 h1:zeFE8Nx11oD6In+f+VDYwGH72t7NV6L5dqaNbDIhB1E=
-github.com/royalcat/kv v0.0.0-20240316134516-1bb692abce73/go.mod h1:Ff0Z/r1H3ojacpEe8SashMKJx6YCIhWrYtpdV8Y/k3A=
+github.com/royalcat/kv v0.0.0-20240318203654-181645f85b10 h1:8vwpCzvVqzNzkYRH9kA3GV5fkWs+8s0jdxtGvswL/MU=
+github.com/royalcat/kv v0.0.0-20240318203654-181645f85b10/go.mod h1:Ff0Z/r1H3ojacpEe8SashMKJx6YCIhWrYtpdV8Y/k3A=
 github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 h1:Lt9DzQALzHoDwMBGJ6v8ObDPR0dzr2a6sXTB1Fq7IHs=
 github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA=
+github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
+github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
+github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
 github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8=
 github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
+github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
+github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
+github.com/samber/slog-multi v1.0.2 h1:6BVH9uHGAsiGkbbtQgAOQJMpKgV8unMrHhhJaw+X1EQ=
+github.com/samber/slog-multi v1.0.2/go.mod h1:uLAvHpGqbYgX4FSL0p1ZwoLuveIAJvBECtE07XmYvFo=
+github.com/samber/slog-zerolog v1.0.0 h1:YpRy0xux1uJr0Ng3wrEjv9nyvb4RAoNqkS611UjzeG8=
+github.com/samber/slog-zerolog v1.0.0/go.mod h1:N2/g/mNGRY1zqsydIYE0uKipSSFsPDjytoVkRnZ0Jp0=
 github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
 github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
 github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
@@ -511,8 +543,8 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1
 github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
 github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs=
-github.com/sosodev/duration v1.1.0 h1:kQcaiGbJaIsRqgQy7VGlZrVw1giWO+lDoX3MCPnpVO4=
-github.com/sosodev/duration v1.1.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
+github.com/sosodev/duration v1.2.0 h1:pqK/FLSjsAADWY74SyWDCjOcd5l7H8GSnnOGEB9A1Us=
+github.com/sosodev/duration v1.2.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -571,10 +603,28 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
 go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
-go.opentelemetry.io/otel v1.8.0 h1:zcvBFizPbpa1q7FehvFiHbQwGzmPILebO0tyqIR5Djg=
-go.opentelemetry.io/otel v1.8.0/go.mod h1:2pkj+iMj0o03Y+cW6/m8Y4WkRdYN3AvCXCnzRMp9yvM=
-go.opentelemetry.io/otel/trace v1.8.0 h1:cSy0DF9eGI5WIfNwZ1q2iUyGj00tGzP24dE1lOlHrfY=
-go.opentelemetry.io/otel/trace v1.8.0/go.mod h1:0Bt3PXY8w+3pheS3hQUt+wow8b1ojPaTBoTCh2zIFI4=
+go.opentelemetry.io/contrib v1.21.1 h1:/U05KZ31iqMqAowhtW10cDPAViNY0tnpAacUgYBmuj8=
+go.opentelemetry.io/contrib v1.21.1/go.mod h1:usW9bPlrjHiJFbK0a6yK/M5wNHs3nLmtrT3vzhoD3co=
+go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
+go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM=
+go.opentelemetry.io/otel/exporters/prometheus v0.46.0 h1:I8WIFXR351FoLJYuloU4EgXbtNX2URfU/85pUPheIEQ=
+go.opentelemetry.io/otel/exporters/prometheus v0.46.0/go.mod h1:ztwVUHe5DTR/1v7PeuGRnU5Bbd4QKYwApWmuutKsJSs=
+go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
+go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
+go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
+go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
+go.opentelemetry.io/otel/sdk/metric v1.24.0 h1:yyMQrPzF+k88/DbH7o4FMAs80puqd+9osbiBrJrz/w8=
+go.opentelemetry.io/otel/sdk/metric v1.24.0/go.mod h1:I6Y5FjH6rvEnTTAYQz3Mmv2kl6Ek5IIrmwTLqMrrOE0=
+go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
+go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
+go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI=
+go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY=
+go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
+go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
 go4.org v0.0.0-20200411211856-f5505b9728dd h1:BNJlw5kRTzdmyfh5U8F93HA2OwkP7ZGwA51eJ/0wKOU=
@@ -718,6 +768,7 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -725,8 +776,9 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
-golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -811,6 +863,12 @@ google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvx
 google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
 google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 h1:YJ5pD9rF8o9Qtta0Cmy9rdBwkSjrTCT6XTiUQVOtIos=
+google.golang.org/genproto v0.0.0-20231212172506-995d672761c0/go.mod h1:l/k7rMz0vFTBPy+tFSGvXEd3z+BcoG1k7EHbqm+YBsY=
+google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 h1:rcS6EyEaoCO52hQDupoSfrxI3R6C2Tq741is7X8OvnM=
+google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917/go.mod h1:CmlNWB9lSezaYELKS5Ym1r44VrrbPUa7JTvw+6MbpJ0=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 h1:6G8oQ016D88m1xAKljMlBOOGWDZkes4kMhgGFlf8WcQ=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU=
 google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
@@ -821,6 +879,8 @@ google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/grpc v1.61.1 h1:kLAiWrZs7YeDM6MumDe7m3y4aM6wacLzM1Y/wiLP9XY=
+google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -833,8 +893,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
-google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
+google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/graphql/query.graphql b/graphql/query.graphql
index 95b7bab..aab94cb 100644
--- a/graphql/query.graphql
+++ b/graphql/query.graphql
@@ -1,5 +1,6 @@
 type Query {
   torrents(filter: TorrentsFilter, pagination: Pagination): [Torrent!]!
+  fsListDir(path: String!): [DirEntry!]!
 }
 
 input TorrentsFilter {
@@ -40,4 +41,4 @@ input DateTimeFilter @oneOf {
 
 input BooleanFilter @oneOf {
   eq: Boolean
-}
\ No newline at end of file
+}
diff --git a/graphql/types/fs.graphql b/graphql/types/fs.graphql
new file mode 100644
index 0000000..73ca119
--- /dev/null
+++ b/graphql/types/fs.graphql
@@ -0,0 +1,26 @@
+interface DirEntry {
+  name: String!
+}
+
+type Dir implements DirEntry {
+  name: String!
+}
+
+type File implements DirEntry {
+  name: String!
+  size: Int!
+}
+
+type ResolverFS implements DirEntry {
+  name: String!
+}
+
+type TorrentFS implements DirEntry {
+  name: String!
+  torrent: Torrent!
+}
+
+type ArchiveFS implements DirEntry {
+  name: String!
+  size: Int!
+}
diff --git a/pkg/rlog/rlog.go b/pkg/rlog/rlog.go
new file mode 100644
index 0000000..c8e1136
--- /dev/null
+++ b/pkg/rlog/rlog.go
@@ -0,0 +1,70 @@
+package rlog
+
+import (
+	"log/slog"
+	"os"
+
+	"github.com/rs/zerolog"
+	slogmulti "github.com/samber/slog-multi"
+	slogzerolog "github.com/samber/slog-zerolog"
+)
+
+const errKey = "error"
+const labelGroupKey = "labelGroup"
+
+var zl = zerolog.New(&zerolog.ConsoleWriter{Out: os.Stderr})
+
+var handlers = []slog.Handler{
+	slogzerolog.Option{Logger: &zl}.NewZerologHandler(),
+}
+
+var defaultLogger = slog.New(slogmulti.Fanout(handlers...))
+
+func init() {
+	slog.SetDefault(defaultLogger)
+}
+
+func AddHandler(nh slog.Handler) {
+	handlers = append(handlers, nh)
+	defaultLogger = slog.New(slogmulti.Fanout(handlers...))
+	slog.SetDefault(defaultLogger)
+}
+
+func ComponentLog(name string) *slog.Logger {
+	return defaultLogger.With(slog.String("component", name))
+}
+
+func ServiceLog(name string) *slog.Logger {
+	return ComponentLog("service/" + name)
+}
+
+func FunctionLog(log *slog.Logger, name string) *slog.Logger {
+	return log.With(slog.String("function", name))
+}
+
+func EndpointLog(log *slog.Logger, name string) *slog.Logger {
+	return log.With(slog.String("endpoint", name))
+}
+
+func Err(err error) slog.Attr {
+	return slog.Attr{Key: errKey, Value: fmtErr(err)}
+}
+
+func Label(args ...any) slog.Attr {
+	return slog.Group(labelGroupKey, args...)
+}
+
+// fmtErr returns a slog.GroupValue with keys "msg" and "trace". If the error
+// does not implement interface { StackTrace() errors.StackTrace }, the "trace"
+// key is omitted.
+func fmtErr(err error) slog.Value {
+	if err == nil {
+		return slog.AnyValue(nil)
+	}
+
+	var groupValues []slog.Attr
+
+	groupValues = append(groupValues, slog.String("msg", err.Error()))
+
+	return slog.GroupValue(groupValues...)
+}
diff --git a/src/config/model.go b/src/config/model.go
index 88ec4b4..3aaf665 100644
--- a/src/config/model.go
+++ b/src/config/model.go
@@ -8,6 +8,8 @@ type Config struct {
 	Log           Log           `koanf:"log"`
 
 	SourceDir string `koanf:"source_dir"`
+
+	OtelHttp string `koanf:"otel_http"`
 }
 
 type WebUi struct {
diff --git a/src/http/api.go b/src/delivery/api.go
similarity index 99%
rename from src/http/api.go
rename to src/delivery/api.go
index 96fba6a..b9b032b 100644
--- a/src/http/api.go
+++ b/src/delivery/api.go
@@ -1,4 +1,4 @@
-package http
+package delivery
 
 import (
 	"bytes"
diff --git a/src/delivery/graphql/generated.go b/src/delivery/graphql/generated.go
index a4f51df..2785488 100644
--- a/src/delivery/graphql/generated.go
+++ b/src/delivery/graphql/generated.go
@@ -52,10 +52,24 @@ type DirectiveRoot struct {
 }
 
 type ComplexityRoot struct {
+	ArchiveFS struct {
+		Name func(childComplexity int) int
+		Size func(childComplexity int) int
+	}
+
+	Dir struct {
+		Name func(childComplexity int) int
+	}
+
 	DownloadTorrentResponse struct {
 		Task func(childComplexity int) int
 	}
 
+	File struct {
+		Name func(childComplexity int) int
+		Size func(childComplexity int) int
+	}
+
 	Mutation struct {
 		CleanupTorrents  func(childComplexity int, files *bool, dryRun bool) int
 		DownloadTorrent  func(childComplexity int, infohash string, file *string) int
@@ -63,7 +77,12 @@ type ComplexityRoot struct {
 	}
 
 	Query struct {
-		Torrents func(childComplexity int, filter *model.TorrentsFilter, pagination *model.Pagination) int
+		FsListDir func(childComplexity int, path string) int
+		Torrents  func(childComplexity int, filter *model.TorrentsFilter, pagination *model.Pagination) int
+	}
+
+	ResolverFS struct {
+		Name func(childComplexity int) int
 	}
 
 	Schema struct {
@@ -91,6 +110,11 @@ type ComplexityRoot struct {
 		TorrentFilePath func(childComplexity int) int
 	}
 
+	TorrentFS struct {
+		Name    func(childComplexity int) int
+		Torrent func(childComplexity int) int
+	}
+
 	TorrentFile struct {
 		BytesCompleted func(childComplexity int) int
 		Filename       func(childComplexity int) int
@@ -119,6 +143,7 @@ type MutationResolver interface {
 }
 type QueryResolver interface {
 	Torrents(ctx context.Context, filter *model.TorrentsFilter, pagination *model.Pagination) ([]*model.Torrent, error)
+	FsListDir(ctx context.Context, path string) ([]model.DirEntry, error)
 }
 type SubscriptionResolver interface {
 	TaskProgress(ctx context.Context, taskID string) (<-chan model.Progress, error)
@@ -151,6 +176,27 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 	_ = ec
 	switch typeName + "." + field {
 
+	case "ArchiveFS.name":
+		if e.complexity.ArchiveFS.Name == nil {
+			break
+		}
+
+		return e.complexity.ArchiveFS.Name(childComplexity), true
+
+	case "ArchiveFS.size":
+		if e.complexity.ArchiveFS.Size == nil {
+			break
+		}
+
+		return e.complexity.ArchiveFS.Size(childComplexity), true
+
+	case "Dir.name":
+		if e.complexity.Dir.Name == nil {
+			break
+		}
+
+		return e.complexity.Dir.Name(childComplexity), true
+
 	case "DownloadTorrentResponse.task":
 		if e.complexity.DownloadTorrentResponse.Task == nil {
 			break
@@ -158,6 +204,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.DownloadTorrentResponse.Task(childComplexity), true
 
+	case "File.name":
+		if e.complexity.File.Name == nil {
+			break
+		}
+
+		return e.complexity.File.Name(childComplexity), true
+
+	case "File.size":
+		if e.complexity.File.Size == nil {
+			break
+		}
+
+		return e.complexity.File.Size(childComplexity), true
+
 	case "Mutation.cleanupTorrents":
 		if e.complexity.Mutation.CleanupTorrents == nil {
 			break
@@ -194,6 +254,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.Mutation.ValidateTorrents(childComplexity, args["filter"].(model.TorrentFilter)), true
 
+	case "Query.fsListDir":
+		if e.complexity.Query.FsListDir == nil {
+			break
+		}
+
+		args, err := ec.field_Query_fsListDir_args(context.TODO(), rawArgs)
+		if err != nil {
+			return 0, false
+		}
+
+		return e.complexity.Query.FsListDir(childComplexity, args["path"].(string)), true
+
 	case "Query.torrents":
 		if e.complexity.Query.Torrents == nil {
 			break
@@ -206,6 +278,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.Query.Torrents(childComplexity, args["filter"].(*model.TorrentsFilter), args["pagination"].(*model.Pagination)), true
 
+	case "ResolverFS.name":
+		if e.complexity.ResolverFS.Name == nil {
+			break
+		}
+
+		return e.complexity.ResolverFS.Name(childComplexity), true
+
 	case "Schema.mutation":
 		if e.complexity.Schema.Mutation == nil {
 			break
@@ -302,6 +381,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.Torrent.TorrentFilePath(childComplexity), true
 
+	case "TorrentFS.name":
+		if e.complexity.TorrentFS.Name == nil {
+			break
+		}
+
+		return e.complexity.TorrentFS.Name(childComplexity), true
+
+	case "TorrentFS.torrent":
+		if e.complexity.TorrentFS.Torrent == nil {
+			break
+		}
+
+		return e.complexity.TorrentFS.Torrent(childComplexity), true
+
 	case "TorrentFile.bytesCompleted":
 		if e.complexity.TorrentFile.BytesCompleted == nil {
 			break
@@ -530,6 +623,7 @@ type Task {
 }`, BuiltIn: false},
 	{Name: "../../../graphql/query.graphql", Input: `type Query {
   torrents(filter: TorrentsFilter, pagination: Pagination): [Torrent!]!
+  fsListDir(path: String!): [DirEntry!]!
 }
 
 input TorrentsFilter {
@@ -570,7 +664,8 @@ input DateTimeFilter @oneOf {
 
 input BooleanFilter @oneOf {
   eq: Boolean
-}`, BuiltIn: false},
+}
+`, BuiltIn: false},
 	{Name: "../../../graphql/schema.graphql", Input: `directive @oneOf on INPUT_OBJECT | FIELD_DEFINITION
 directive @stream on FIELD_DEFINITION
 
@@ -580,7 +675,6 @@ type Schema {
   query: Query
   mutation: Mutation
 }
-
 `, BuiltIn: false},
 	{Name: "../../../graphql/subscription.graphql", Input: `type Subscription {
     taskProgress(taskID: ID!): Progress
@@ -598,6 +692,33 @@ interface Progress {
     current: Int!
     total: Int!
 }`, BuiltIn: false},
+	{Name: "../../../graphql/types/fs.graphql", Input: `interface DirEntry {
+  name: String!
+}
+
+type Dir implements DirEntry {
+  name: String!
+}
+
+type File implements DirEntry {
+  name: String!
+  size: Int!
+}
+
+type ResolverFS implements DirEntry {
+  name: String!
+}
+
+type TorrentFS implements DirEntry {
+  name: String!
+  torrent: Torrent!
+}
+
+type ArchiveFS implements DirEntry {
+  name: String!
+  size: Int!
+}
+`, BuiltIn: false},
 	{Name: "../../../graphql/types/torrent.graphql", Input: `type Torrent {
   name: String!
   infohash: String!
@@ -707,6 +828,21 @@ func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs
 	return args, nil
 }
 
+func (ec *executionContext) field_Query_fsListDir_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
+	var err error
+	args := map[string]interface{}{}
+	var arg0 string
+	if tmp, ok := rawArgs["path"]; ok {
+		ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("path"))
+		arg0, err = ec.unmarshalNString2string(ctx, tmp)
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["path"] = arg0
+	return args, nil
+}
+
 func (ec *executionContext) field_Query_torrents_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
 	var err error
 	args := map[string]interface{}{}
@@ -784,6 +920,138 @@ func (ec *executionContext) field___Type_fields_args(ctx context.Context, rawArg
 
 // region    **************************** field.gotpl *****************************
 
+func (ec *executionContext) _ArchiveFS_name(ctx context.Context, field graphql.CollectedField, obj *model.ArchiveFs) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_ArchiveFS_name(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Name, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(string)
+	fc.Result = res
+	return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_ArchiveFS_name(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "ArchiveFS",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type String does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _ArchiveFS_size(ctx context.Context, field graphql.CollectedField, obj *model.ArchiveFs) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_ArchiveFS_size(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Size, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(int64)
+	fc.Result = res
+	return ec.marshalNInt2int64(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_ArchiveFS_size(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "ArchiveFS",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Int does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _Dir_name(ctx context.Context, field graphql.CollectedField, obj *model.Dir) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_Dir_name(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Name, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(string)
+	fc.Result = res
+	return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Dir_name(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "Dir",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type String does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
 func (ec *executionContext) _DownloadTorrentResponse_task(ctx context.Context, field graphql.CollectedField, obj *model.DownloadTorrentResponse) (ret graphql.Marshaler) {
 	fc, err := ec.fieldContext_DownloadTorrentResponse_task(ctx, field)
 	if err != nil {
@@ -829,6 +1097,94 @@ func (ec *executionContext) fieldContext_DownloadTorrentResponse_task(ctx contex
 	return fc, nil
 }
 
+func (ec *executionContext) _File_name(ctx context.Context, field graphql.CollectedField, obj *model.File) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_File_name(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Name, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(string)
+	fc.Result = res
+	return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_File_name(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "File",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type String does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _File_size(ctx context.Context, field graphql.CollectedField, obj *model.File) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_File_size(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Size, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(int64)
+	fc.Result = res
+	return ec.marshalNInt2int64(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_File_size(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "File",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Int does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
 func (ec *executionContext) _Mutation_validateTorrents(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
 	fc, err := ec.fieldContext_Mutation_validateTorrents(ctx, field)
 	if err != nil {
@@ -1068,6 +1424,61 @@ func (ec *executionContext) fieldContext_Query_torrents(ctx context.Context, fie
 	return fc, nil
 }
 
+func (ec *executionContext) _Query_fsListDir(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_Query_fsListDir(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return ec.resolvers.Query().FsListDir(rctx, fc.Args["path"].(string))
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.([]model.DirEntry)
+	fc.Result = res
+	return ec.marshalNDirEntry2ᚕgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐDirEntryᚄ(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Query_fsListDir(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "Query",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: true,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("FieldContext.Child cannot be called on type INTERFACE")
+		},
+	}
+	defer func() {
+		if r := recover(); r != nil {
+			err = ec.Recover(ctx, r)
+			ec.Error(ctx, err)
+		}
+	}()
+	ctx = graphql.WithFieldContext(ctx, fc)
+	if fc.Args, err = ec.field_Query_fsListDir_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
+		ec.Error(ctx, err)
+		return fc, err
+	}
+	return fc, nil
+}
+
 func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
 	fc, err := ec.fieldContext_Query___type(ctx, field)
 	if err != nil {
@@ -1197,6 +1608,50 @@ func (ec *executionContext) fieldContext_Query___schema(ctx context.Context, fie
 	return fc, nil
 }
 
+func (ec *executionContext) _ResolverFS_name(ctx context.Context, field graphql.CollectedField, obj *model.ResolverFs) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_ResolverFS_name(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Name, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(string)
+	fc.Result = res
+	return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_ResolverFS_name(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "ResolverFS",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type String does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
 func (ec *executionContext) _Schema_query(ctx context.Context, field graphql.CollectedField, obj *model.Schema) (ret graphql.Marshaler) {
 	fc, err := ec.fieldContext_Schema_query(ctx, field)
 	if err != nil {
@@ -1224,6 +1679,8 @@ func (ec *executionContext) fieldContext_Schema_query(ctx context.Context, field
 			switch field.Name {
 			case "torrents":
 				return ec.fieldContext_Query_torrents(ctx, field)
+			case "fsListDir":
+				return ec.fieldContext_Query_fsListDir(ctx, field)
 			case "__schema":
 				return ec.fieldContext_Query___schema(ctx, field)
 			case "__type":
@@ -1826,6 +2283,112 @@ func (ec *executionContext) fieldContext_Torrent_peers(ctx context.Context, fiel
 	return fc, nil
 }
 
+func (ec *executionContext) _TorrentFS_name(ctx context.Context, field graphql.CollectedField, obj *model.TorrentFs) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentFS_name(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Name, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(string)
+	fc.Result = res
+	return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_TorrentFS_name(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "TorrentFS",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type String does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _TorrentFS_torrent(ctx context.Context, field graphql.CollectedField, obj *model.TorrentFs) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_TorrentFS_torrent(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Torrent, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(*model.Torrent)
+	fc.Result = res
+	return ec.marshalNTorrent2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrent(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_TorrentFS_torrent(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "TorrentFS",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			switch field.Name {
+			case "name":
+				return ec.fieldContext_Torrent_name(ctx, field)
+			case "infohash":
+				return ec.fieldContext_Torrent_infohash(ctx, field)
+			case "bytesCompleted":
+				return ec.fieldContext_Torrent_bytesCompleted(ctx, field)
+			case "torrentFilePath":
+				return ec.fieldContext_Torrent_torrentFilePath(ctx, field)
+			case "bytesMissing":
+				return ec.fieldContext_Torrent_bytesMissing(ctx, field)
+			case "files":
+				return ec.fieldContext_Torrent_files(ctx, field)
+			case "excludedFiles":
+				return ec.fieldContext_Torrent_excludedFiles(ctx, field)
+			case "peers":
+				return ec.fieldContext_Torrent_peers(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type Torrent", field.Name)
+		},
+	}
+	return fc, nil
+}
+
 func (ec *executionContext) _TorrentFile_filename(ctx context.Context, field graphql.CollectedField, obj *model.TorrentFile) (ret graphql.Marshaler) {
 	fc, err := ec.fieldContext_TorrentFile_filename(ctx, field)
 	if err != nil {
@@ -4625,32 +5188,100 @@ func (ec *executionContext) unmarshalInputTorrentsFilter(ctx context.Context, ob
 		switch k {
 		case "name":
 			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name"))
-			data, err := ec.unmarshalOStringFilter2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐStringFilter(ctx, v)
-			if err != nil {
-				return it, err
+			directive0 := func(ctx context.Context) (interface{}, error) {
+				return ec.unmarshalOStringFilter2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐStringFilter(ctx, v)
+			}
+			directive1 := func(ctx context.Context) (interface{}, error) {
+				if ec.directives.OneOf == nil {
+					return nil, errors.New("directive oneOf is not implemented")
+				}
+				return ec.directives.OneOf(ctx, obj, directive0)
+			}
+
+			tmp, err := directive1(ctx)
+			if err != nil {
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+			if data, ok := tmp.(*model.StringFilter); ok {
+				it.Name = data
+			} else if tmp == nil {
+				it.Name = nil
+			} else {
+				err := fmt.Errorf(`unexpected type %T from directive, should be *git.kmsign.ru/royalcat/tstor/src/delivery/graphql/model.StringFilter`, tmp)
+				return it, graphql.ErrorOnPath(ctx, err)
 			}
-			it.Name = data
 		case "bytesCompleted":
 			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("bytesCompleted"))
-			data, err := ec.unmarshalOIntFilter2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐIntFilter(ctx, v)
-			if err != nil {
-				return it, err
+			directive0 := func(ctx context.Context) (interface{}, error) {
+				return ec.unmarshalOIntFilter2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐIntFilter(ctx, v)
+			}
+			directive1 := func(ctx context.Context) (interface{}, error) {
+				if ec.directives.OneOf == nil {
+					return nil, errors.New("directive oneOf is not implemented")
+				}
+				return ec.directives.OneOf(ctx, obj, directive0)
+			}
+
+			tmp, err := directive1(ctx)
+			if err != nil {
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+			if data, ok := tmp.(*model.IntFilter); ok {
+				it.BytesCompleted = data
+			} else if tmp == nil {
+				it.BytesCompleted = nil
+			} else {
+				err := fmt.Errorf(`unexpected type %T from directive, should be *git.kmsign.ru/royalcat/tstor/src/delivery/graphql/model.IntFilter`, tmp)
+				return it, graphql.ErrorOnPath(ctx, err)
 			}
-			it.BytesCompleted = data
 		case "bytesMissing":
 			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("bytesMissing"))
-			data, err := ec.unmarshalOIntFilter2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐIntFilter(ctx, v)
-			if err != nil {
-				return it, err
+			directive0 := func(ctx context.Context) (interface{}, error) {
+				return ec.unmarshalOIntFilter2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐIntFilter(ctx, v)
+			}
+			directive1 := func(ctx context.Context) (interface{}, error) {
+				if ec.directives.OneOf == nil {
+					return nil, errors.New("directive oneOf is not implemented")
+				}
+				return ec.directives.OneOf(ctx, obj, directive0)
+			}
+
+			tmp, err := directive1(ctx)
+			if err != nil {
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+			if data, ok := tmp.(*model.IntFilter); ok {
+				it.BytesMissing = data
+			} else if tmp == nil {
+				it.BytesMissing = nil
+			} else {
+				err := fmt.Errorf(`unexpected type %T from directive, should be *git.kmsign.ru/royalcat/tstor/src/delivery/graphql/model.IntFilter`, tmp)
+				return it, graphql.ErrorOnPath(ctx, err)
 			}
-			it.BytesMissing = data
 		case "peersCount":
 			ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("peersCount"))
-			data, err := ec.unmarshalOIntFilter2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐIntFilter(ctx, v)
-			if err != nil {
-				return it, err
+			directive0 := func(ctx context.Context) (interface{}, error) {
+				return ec.unmarshalOIntFilter2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐIntFilter(ctx, v)
+			}
+			directive1 := func(ctx context.Context) (interface{}, error) {
+				if ec.directives.OneOf == nil {
+					return nil, errors.New("directive oneOf is not implemented")
+				}
+				return ec.directives.OneOf(ctx, obj, directive0)
+			}
+
+			tmp, err := directive1(ctx)
+			if err != nil {
+				return it, graphql.ErrorOnPath(ctx, err)
+			}
+			if data, ok := tmp.(*model.IntFilter); ok {
+				it.PeersCount = data
+			} else if tmp == nil {
+				it.PeersCount = nil
+			} else {
+				err := fmt.Errorf(`unexpected type %T from directive, should be *git.kmsign.ru/royalcat/tstor/src/delivery/graphql/model.IntFilter`, tmp)
+				return it, graphql.ErrorOnPath(ctx, err)
 			}
-			it.PeersCount = data
 		}
 	}
 
@@ -4661,6 +5292,50 @@ func (ec *executionContext) unmarshalInputTorrentsFilter(ctx context.Context, ob
 
 // region    ************************** interface.gotpl ***************************
 
+func (ec *executionContext) _DirEntry(ctx context.Context, sel ast.SelectionSet, obj model.DirEntry) graphql.Marshaler {
+	switch obj := (obj).(type) {
+	case nil:
+		return graphql.Null
+	case model.Dir:
+		return ec._Dir(ctx, sel, &obj)
+	case *model.Dir:
+		if obj == nil {
+			return graphql.Null
+		}
+		return ec._Dir(ctx, sel, obj)
+	case model.File:
+		return ec._File(ctx, sel, &obj)
+	case *model.File:
+		if obj == nil {
+			return graphql.Null
+		}
+		return ec._File(ctx, sel, obj)
+	case model.ResolverFs:
+		return ec._ResolverFS(ctx, sel, &obj)
+	case *model.ResolverFs:
+		if obj == nil {
+			return graphql.Null
+		}
+		return ec._ResolverFS(ctx, sel, obj)
+	case model.TorrentFs:
+		return ec._TorrentFS(ctx, sel, &obj)
+	case *model.TorrentFs:
+		if obj == nil {
+			return graphql.Null
+		}
+		return ec._TorrentFS(ctx, sel, obj)
+	case model.ArchiveFs:
+		return ec._ArchiveFS(ctx, sel, &obj)
+	case *model.ArchiveFs:
+		if obj == nil {
+			return graphql.Null
+		}
+		return ec._ArchiveFS(ctx, sel, obj)
+	default:
+		panic(fmt.Errorf("unexpected type %T", obj))
+	}
+}
+
 func (ec *executionContext) _Progress(ctx context.Context, sel ast.SelectionSet, obj model.Progress) graphql.Marshaler {
 	switch obj := (obj).(type) {
 	case nil:
@@ -4681,6 +5356,89 @@ func (ec *executionContext) _Progress(ctx context.Context, sel ast.SelectionSet,
 
 // region    **************************** object.gotpl ****************************
 
+var archiveFSImplementors = []string{"ArchiveFS", "DirEntry"}
+
+func (ec *executionContext) _ArchiveFS(ctx context.Context, sel ast.SelectionSet, obj *model.ArchiveFs) graphql.Marshaler {
+	fields := graphql.CollectFields(ec.OperationContext, sel, archiveFSImplementors)
+
+	out := graphql.NewFieldSet(fields)
+	deferred := make(map[string]*graphql.FieldSet)
+	for i, field := range fields {
+		switch field.Name {
+		case "__typename":
+			out.Values[i] = graphql.MarshalString("ArchiveFS")
+		case "name":
+			out.Values[i] = ec._ArchiveFS_name(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "size":
+			out.Values[i] = ec._ArchiveFS_size(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		default:
+			panic("unknown field " + strconv.Quote(field.Name))
+		}
+	}
+	out.Dispatch(ctx)
+	if out.Invalids > 0 {
+		return graphql.Null
+	}
+
+	atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+	for label, dfs := range deferred {
+		ec.processDeferredGroup(graphql.DeferredGroup{
+			Label:    label,
+			Path:     graphql.GetPath(ctx),
+			FieldSet: dfs,
+			Context:  ctx,
+		})
+	}
+
+	return out
+}
+
+var dirImplementors = []string{"Dir", "DirEntry"}
+
+func (ec *executionContext) _Dir(ctx context.Context, sel ast.SelectionSet, obj *model.Dir) graphql.Marshaler {
+	fields := graphql.CollectFields(ec.OperationContext, sel, dirImplementors)
+
+	out := graphql.NewFieldSet(fields)
+	deferred := make(map[string]*graphql.FieldSet)
+	for i, field := range fields {
+		switch field.Name {
+		case "__typename":
+			out.Values[i] = graphql.MarshalString("Dir")
+		case "name":
+			out.Values[i] = ec._Dir_name(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		default:
+			panic("unknown field " + strconv.Quote(field.Name))
+		}
+	}
+	out.Dispatch(ctx)
+	if out.Invalids > 0 {
+		return graphql.Null
+	}
+
+	atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+	for label, dfs := range deferred {
+		ec.processDeferredGroup(graphql.DeferredGroup{
+			Label:    label,
+			Path:     graphql.GetPath(ctx),
+			FieldSet: dfs,
+			Context:  ctx,
+		})
+	}
+
+	return out
+}
+
 var downloadTorrentResponseImplementors = []string{"DownloadTorrentResponse"}
 
 func (ec *executionContext) _DownloadTorrentResponse(ctx context.Context, sel ast.SelectionSet, obj *model.DownloadTorrentResponse) graphql.Marshaler {
@@ -4717,6 +5475,50 @@ func (ec *executionContext) _DownloadTorrentResponse(ctx context.Context, sel as
 	return out
 }
 
+var fileImplementors = []string{"File", "DirEntry"}
+
+func (ec *executionContext) _File(ctx context.Context, sel ast.SelectionSet, obj *model.File) graphql.Marshaler {
+	fields := graphql.CollectFields(ec.OperationContext, sel, fileImplementors)
+
+	out := graphql.NewFieldSet(fields)
+	deferred := make(map[string]*graphql.FieldSet)
+	for i, field := range fields {
+		switch field.Name {
+		case "__typename":
+			out.Values[i] = graphql.MarshalString("File")
+		case "name":
+			out.Values[i] = ec._File_name(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "size":
+			out.Values[i] = ec._File_size(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		default:
+			panic("unknown field " + strconv.Quote(field.Name))
+		}
+	}
+	out.Dispatch(ctx)
+	if out.Invalids > 0 {
+		return graphql.Null
+	}
+
+	atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+	for label, dfs := range deferred {
+		ec.processDeferredGroup(graphql.DeferredGroup{
+			Label:    label,
+			Path:     graphql.GetPath(ctx),
+			FieldSet: dfs,
+			Context:  ctx,
+		})
+	}
+
+	return out
+}
+
 var mutationImplementors = []string{"Mutation"}
 
 func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler {
@@ -4817,6 +5619,28 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
 					func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
 			}
 
+			out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) })
+		case "fsListDir":
+			field := field
+
+			innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
+				defer func() {
+					if r := recover(); r != nil {
+						ec.Error(ctx, ec.Recover(ctx, r))
+					}
+				}()
+				res = ec._Query_fsListDir(ctx, field)
+				if res == graphql.Null {
+					atomic.AddUint32(&fs.Invalids, 1)
+				}
+				return res
+			}
+
+			rrm := func(ctx context.Context) graphql.Marshaler {
+				return ec.OperationContext.RootResolverMiddleware(ctx,
+					func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
+			}
+
 			out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) })
 		case "__type":
 			out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
@@ -4849,6 +5673,45 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
 	return out
 }
 
+var resolverFSImplementors = []string{"ResolverFS", "DirEntry"}
+
+func (ec *executionContext) _ResolverFS(ctx context.Context, sel ast.SelectionSet, obj *model.ResolverFs) graphql.Marshaler {
+	fields := graphql.CollectFields(ec.OperationContext, sel, resolverFSImplementors)
+
+	out := graphql.NewFieldSet(fields)
+	deferred := make(map[string]*graphql.FieldSet)
+	for i, field := range fields {
+		switch field.Name {
+		case "__typename":
+			out.Values[i] = graphql.MarshalString("ResolverFS")
+		case "name":
+			out.Values[i] = ec._ResolverFS_name(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		default:
+			panic("unknown field " + strconv.Quote(field.Name))
+		}
+	}
+	out.Dispatch(ctx)
+	if out.Invalids > 0 {
+		return graphql.Null
+	}
+
+	atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+	for label, dfs := range deferred {
+		ec.processDeferredGroup(graphql.DeferredGroup{
+			Label:    label,
+			Path:     graphql.GetPath(ctx),
+			FieldSet: dfs,
+			Context:  ctx,
+		})
+	}
+
+	return out
+}
+
 var schemaImplementors = []string{"Schema"}
 
 func (ec *executionContext) _Schema(ctx context.Context, sel ast.SelectionSet, obj *model.Schema) graphql.Marshaler {
@@ -5146,6 +6009,50 @@ func (ec *executionContext) _Torrent(ctx context.Context, sel ast.SelectionSet,
 	return out
 }
 
+var torrentFSImplementors = []string{"TorrentFS", "DirEntry"}
+
+func (ec *executionContext) _TorrentFS(ctx context.Context, sel ast.SelectionSet, obj *model.TorrentFs) graphql.Marshaler {
+	fields := graphql.CollectFields(ec.OperationContext, sel, torrentFSImplementors)
+
+	out := graphql.NewFieldSet(fields)
+	deferred := make(map[string]*graphql.FieldSet)
+	for i, field := range fields {
+		switch field.Name {
+		case "__typename":
+			out.Values[i] = graphql.MarshalString("TorrentFS")
+		case "name":
+			out.Values[i] = ec._TorrentFS_name(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "torrent":
+			out.Values[i] = ec._TorrentFS_torrent(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		default:
+			panic("unknown field " + strconv.Quote(field.Name))
+		}
+	}
+	out.Dispatch(ctx)
+	if out.Invalids > 0 {
+		return graphql.Null
+	}
+
+	atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+	for label, dfs := range deferred {
+		ec.processDeferredGroup(graphql.DeferredGroup{
+			Label:    label,
+			Path:     graphql.GetPath(ctx),
+			FieldSet: dfs,
+			Context:  ctx,
+		})
+	}
+
+	return out
+}
+
 var torrentFileImplementors = []string{"TorrentFile"}
 
 func (ec *executionContext) _TorrentFile(ctx context.Context, sel ast.SelectionSet, obj *model.TorrentFile) graphql.Marshaler {
@@ -5644,6 +6551,60 @@ func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.Se
 	return res
 }
 
+func (ec *executionContext) marshalNDirEntry2gitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐDirEntry(ctx context.Context, sel ast.SelectionSet, v model.DirEntry) graphql.Marshaler {
+	if v == nil {
+		if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+			ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+		}
+		return graphql.Null
+	}
+	return ec._DirEntry(ctx, sel, v)
+}
+
+func (ec *executionContext) marshalNDirEntry2ᚕgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐDirEntryᚄ(ctx context.Context, sel ast.SelectionSet, v []model.DirEntry) graphql.Marshaler {
+	ret := make(graphql.Array, len(v))
+	var wg sync.WaitGroup
+	isLen1 := len(v) == 1
+	if !isLen1 {
+		wg.Add(len(v))
+	}
+	for i := range v {
+		i := i
+		fc := &graphql.FieldContext{
+			Index:  &i,
+			Result: &v[i],
+		}
+		ctx := graphql.WithFieldContext(ctx, fc)
+		f := func(i int) {
+			defer func() {
+				if r := recover(); r != nil {
+					ec.Error(ctx, ec.Recover(ctx, r))
+					ret = nil
+				}
+			}()
+			if !isLen1 {
+				defer wg.Done()
+			}
+			ret[i] = ec.marshalNDirEntry2gitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐDirEntry(ctx, sel, v[i])
+		}
+		if isLen1 {
+			f(i)
+		} else {
+			go f(i)
+		}
+
+	}
+	wg.Wait()
+
+	for _, e := range ret {
+		if e == graphql.Null {
+			return graphql.Null
+		}
+	}
+
+	return ret
+}
+
 func (ec *executionContext) unmarshalNFloat2float64(ctx context.Context, v interface{}) (float64, error) {
 	res, err := graphql.UnmarshalFloatContext(ctx, v)
 	return res, graphql.ErrorOnPath(ctx, err)
diff --git a/src/delivery/graphql/model/models_gen.go b/src/delivery/graphql/model/models_gen.go
index deb4b6d..4a56ee6 100644
--- a/src/delivery/graphql/model/models_gen.go
+++ b/src/delivery/graphql/model/models_gen.go
@@ -9,12 +9,25 @@ import (
 	"github.com/anacrolix/torrent"
 )
 
+type DirEntry interface {
+	IsDirEntry()
+	GetName() string
+}
+
 type Progress interface {
 	IsProgress()
 	GetCurrent() int64
 	GetTotal() int64
 }
 
+type ArchiveFs struct {
+	Name string `json:"name"`
+	Size int64  `json:"size"`
+}
+
+func (ArchiveFs) IsDirEntry()          {}
+func (this ArchiveFs) GetName() string { return this.Name }
+
 type BooleanFilter struct {
 	Eq *bool `json:"eq,omitempty"`
 }
@@ -27,10 +40,25 @@ type DateTimeFilter struct {
 	Lte *time.Time `json:"lte,omitempty"`
 }
 
+type Dir struct {
+	Name string `json:"name"`
+}
+
+func (Dir) IsDirEntry()          {}
+func (this Dir) GetName() string { return this.Name }
+
 type DownloadTorrentResponse struct {
 	Task *Task `json:"task,omitempty"`
 }
 
+type File struct {
+	Name string `json:"name"`
+	Size int64  `json:"size"`
+}
+
+func (File) IsDirEntry()          {}
+func (this File) GetName() string { return this.Name }
+
 type IntFilter struct {
 	Eq  *int64  `json:"eq,omitempty"`
 	Gt  *int64  `json:"gt,omitempty"`
@@ -51,6 +79,13 @@ type Pagination struct {
 type Query struct {
 }
 
+type ResolverFs struct {
+	Name string `json:"name"`
+}
+
+func (ResolverFs) IsDirEntry()          {}
+func (this ResolverFs) GetName() string { return this.Name }
+
 type Schema struct {
 	Query    *Query    `json:"query,omitempty"`
 	Mutation *Mutation `json:"mutation,omitempty"`
@@ -81,6 +116,14 @@ type Torrent struct {
 	T               *controller.Torrent `json:"-"`
 }
 
+type TorrentFs struct {
+	Name    string   `json:"name"`
+	Torrent *Torrent `json:"torrent"`
+}
+
+func (TorrentFs) IsDirEntry()          {}
+func (this TorrentFs) GetName() string { return this.Name }
+
 type TorrentFile struct {
 	Filename       string        `json:"filename"`
 	Size           int64         `json:"size"`
diff --git a/src/delivery/graphql/resolver/query.resolvers.go b/src/delivery/graphql/resolver/query.resolvers.go
index b4687a7..30ee852 100644
--- a/src/delivery/graphql/resolver/query.resolvers.go
+++ b/src/delivery/graphql/resolver/query.resolvers.go
@@ -9,6 +9,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/vfs"
 )
 
 // Torrents is the resolver for the torrents field.
@@ -63,6 +64,42 @@ func (r *queryResolver) Torrents(ctx context.Context, filter *model.TorrentsFilt
 	return tr, nil
 }
 
+// FsListDir is the resolver for the fsListDir field.
+func (r *queryResolver) FsListDir(ctx context.Context, path string) ([]model.DirEntry, error) {
+	entries, err := r.VFS.ReadDir(path)
+	if err != nil {
+		return nil, err
+	}
+	out := []model.DirEntry{}
+	for _, e := range entries {
+		switch e.(type) {
+		case *vfs.TorrentFs:
+			e := e.(*vfs.TorrentFs)
+			out = append(out, model.TorrentFs{
+				Name:    e.Name(),
+				Torrent: model.MapTorrent(e.Torrent),
+			})
+		default:
+			if e.IsDir() {
+				out = append(out, model.Dir{Name: e.Name()})
+			} else {
+				info, err := e.Info()
+				if err != nil {
+					return nil, err
+				}
+
+				out = append(out, model.File{
+					Name: e.Name(),
+					Size: info.Size(),
+				})
+			}
+		}
+
+	}
+
+	return out, nil
+}
+
 // Query returns graph.QueryResolver implementation.
 func (r *Resolver) Query() graph.QueryResolver { return &queryResolver{r} }
 
diff --git a/src/delivery/graphql/resolver/resolver.go b/src/delivery/graphql/resolver/resolver.go
index 1e937e1..9a76120 100644
--- a/src/delivery/graphql/resolver/resolver.go
+++ b/src/delivery/graphql/resolver/resolver.go
@@ -1,6 +1,9 @@
 package resolver
 
-import "git.kmsign.ru/royalcat/tstor/src/host/service"
+import (
+	"git.kmsign.ru/royalcat/tstor/src/host/service"
+	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
+)
 
 // This file will not be regenerated automatically.
 //
@@ -8,4 +11,5 @@ import "git.kmsign.ru/royalcat/tstor/src/host/service"
 
 type Resolver struct {
 	Service *service.Service
+	VFS     vfs.Filesystem
 }
diff --git a/src/http/http.go b/src/delivery/http.go
similarity index 91%
rename from src/http/http.go
rename to src/delivery/http.go
index 495949a..244ec16 100644
--- a/src/http/http.go
+++ b/src/delivery/http.go
@@ -1,4 +1,4 @@
-package http
+package delivery
 
 import (
 	"fmt"
@@ -7,15 +7,15 @@ import (
 
 	"git.kmsign.ru/royalcat/tstor"
 	"git.kmsign.ru/royalcat/tstor/src/config"
-	"git.kmsign.ru/royalcat/tstor/src/delivery"
 	"git.kmsign.ru/royalcat/tstor/src/host/service"
+	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
 	"github.com/anacrolix/missinggo/v2/filecache"
 	"github.com/gin-contrib/pprof"
 	"github.com/gin-gonic/gin"
 	"github.com/shurcooL/httpfs/html/vfstemplate"
 )
 
-func New(fc *filecache.Cache, ss *service.Stats, s *service.Service, logPath string, cfg *config.Config) error {
+func New(fc *filecache.Cache, ss *service.Stats, s *service.Service, vfs vfs.Filesystem, logPath string, cfg *config.Config) error {
 	log := slog.With()
 
 	gin.SetMode(gin.ReleaseMode)
@@ -40,18 +40,16 @@ func New(fc *filecache.Cache, ss *service.Stats, s *service.Service, logPath str
 	// r.GET("/routes", routesHandler(ss))
 	r.GET("/logs", logsHandler)
 	r.GET("/servers", serversFoldersHandler())
-	r.Any("/graphql", gin.WrapH(delivery.GraphQLHandler(s)))
+	r.Any("/graphql", gin.WrapH(GraphQLHandler(s, vfs)))
 
 	api := r.Group("/api")
 	{
 		api.GET("/log", apiLogHandler(logPath))
 		api.GET("/status", apiStatusHandler(fc, ss))
 		// api.GET("/servers", apiServersHandler(tss))
-
 		// api.GET("/routes", apiRoutesHandler(ss))
 		// api.POST("/routes/:route/torrent", apiAddTorrentHandler(s))
 		// api.DELETE("/routes/:route/torrent/:torrent_hash", apiDelTorrentHandler(s))
-
 	}
 
 	log.Info("starting webserver", "host", fmt.Sprintf("%s:%d", cfg.WebUi.IP, cfg.WebUi.Port))
diff --git a/src/http/model.go b/src/delivery/model.go
similarity index 88%
rename from src/http/model.go
rename to src/delivery/model.go
index c752eb0..3088d80 100644
--- a/src/http/model.go
+++ b/src/delivery/model.go
@@ -1,4 +1,4 @@
-package http
+package delivery
 
 type RouteAdd struct {
 	Magnet string `json:"magnet" binding:"required"`
diff --git a/src/delivery/router.go b/src/delivery/router.go
index 4c5f0c4..d03b2ff 100644
--- a/src/delivery/router.go
+++ b/src/delivery/router.go
@@ -6,23 +6,26 @@ import (
 	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/vfs"
 	"github.com/99designs/gqlgen/graphql/handler"
 	"github.com/99designs/gqlgen/graphql/handler/extension"
 	"github.com/99designs/gqlgen/graphql/handler/lru"
 	"github.com/99designs/gqlgen/graphql/handler/transport"
+	"github.com/ravilushqa/otelgqlgen"
 )
 
-func GraphQLHandler(service *service.Service) http.Handler {
+func GraphQLHandler(service *service.Service, vfs vfs.Filesystem) http.Handler {
 	graphqlHandler := handler.NewDefaultServer(
 		graph.NewExecutableSchema(
 			graph.Config{
-				Resolvers: &resolver.Resolver{Service: service},
+				Resolvers: &resolver.Resolver{Service: service, VFS: vfs},
 				Directives: graph.DirectiveRoot{
 					OneOf: graph.OneOf,
 				},
 			},
 		),
 	)
+
 	graphqlHandler.AddTransport(&transport.POST{})
 	graphqlHandler.AddTransport(&transport.Websocket{})
 	graphqlHandler.AddTransport(&transport.SSE{})
@@ -30,6 +33,7 @@ func GraphQLHandler(service *service.Service) http.Handler {
 	graphqlHandler.SetQueryCache(lru.New(1000))
 	graphqlHandler.Use(extension.Introspection{})
 	graphqlHandler.Use(extension.AutomaticPersistedQuery{Cache: lru.New(100)})
+	graphqlHandler.Use(otelgqlgen.Middleware())
 
 	return graphqlHandler
 }
diff --git a/src/http/web.go b/src/delivery/web.go
similarity index 96%
rename from src/http/web.go
rename to src/delivery/web.go
index 860347f..9bcc202 100644
--- a/src/http/web.go
+++ b/src/delivery/web.go
@@ -1,4 +1,4 @@
-package http
+package delivery
 
 import (
 	"net/http"
diff --git a/src/host/service/service.go b/src/host/service/service.go
index 4388058..1d42a6a 100644
--- a/src/host/service/service.go
+++ b/src/host/service/service.go
@@ -5,24 +5,33 @@ import (
 	"fmt"
 	"log/slog"
 	"os"
+	"path"
 	"path/filepath"
 	"slices"
 	"strings"
 	"time"
 
+	"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/vfs"
 	"go.uber.org/multierr"
+	"golang.org/x/exp/maps"
 
 	"github.com/anacrolix/torrent"
 	"github.com/anacrolix/torrent/bencode"
 	"github.com/anacrolix/torrent/metainfo"
 	"github.com/anacrolix/torrent/types"
 	"github.com/anacrolix/torrent/types/infohash"
+	"github.com/royalcat/kv"
 )
 
+type DirAquire struct {
+	Name   string
+	Hashes []infohash.T
+}
+
 type Service struct {
 	c             *torrent.Client
 	excludedFiles *store.FilesMappings
@@ -35,11 +44,21 @@ type Service struct {
 	Storage         datastorage.DataStorage
 	SourceDir       string
 
+	dirsAquire kv.Store[string, DirAquire]
+
 	log                     *slog.Logger
 	addTimeout, readTimeout int
 }
 
-func NewService(sourceDir string, c *torrent.Client, storage datastorage.DataStorage, excludedFiles *store.FilesMappings, infoBytes *store.InfoBytes, addTimeout, readTimeout int) *Service {
+func NewService(sourceDir string, cfg config.TorrentClient, c *torrent.Client,
+	storage datastorage.DataStorage, excludedFiles *store.FilesMappings, infoBytes *store.InfoBytes,
+	addTimeout, readTimeout int,
+) (*Service, error) {
+	dirsAcquire, err := kv.NewBadgerKV[string, DirAquire](path.Join(cfg.MetadataFolder, "dir-acquire"))
+	if err != nil {
+		return nil, err
+	}
+
 	s := &Service{
 		log:             slog.With("component", "torrent-service"),
 		c:               c,
@@ -49,6 +68,7 @@ func NewService(sourceDir string, c *torrent.Client, storage datastorage.DataSto
 		Storage:         storage,
 		SourceDir:       sourceDir,
 		torrentLoaded:   make(chan struct{}),
+		dirsAquire:      dirsAcquire,
 		// stats:       newStats(), // TODO persistent
 		addTimeout:  addTimeout,
 		readTimeout: readTimeout,
@@ -62,7 +82,7 @@ func NewService(sourceDir string, c *torrent.Client, storage datastorage.DataSto
 		close(s.torrentLoaded)
 	}()
 
-	return s
+	return s, nil
 }
 
 var _ vfs.FsFactory = (*Service)(nil).NewTorrentFs
@@ -109,17 +129,16 @@ func (s *Service) AddTorrent(ctx context.Context, f vfs.File) (*torrent.Torrent,
 		if err != nil {
 			infoBytes = nil
 		} else {
-			// for _, t := range s.c.Torrents() {
-			// 	if t.Name() == info.BestName() && t.InfoHash() != spec.InfoHash {
-			// 		<-t.GotInfo()
-			// 		if !isTorrentCompatable(*t.Info(), info) {
-			// 			return nil, fmt.Errorf(
-			// 				"torrent with name '%s' not compatable existing infohash: %s, new: %s",
-			// 				t.Name(), t.InfoHash().HexString(), spec.InfoHash.HexString(),
-			// 			)
-			// 		}
-			// 	}
-			// }
+			compatable, _, err := s.checkTorrentCompatable(ctx, spec.InfoHash, info)
+			if err != nil {
+				return nil, err
+			}
+			if !compatable {
+				return nil, fmt.Errorf(
+					"torrent with name '%s' not compatable existing infohash: %s, new: %s",
+					t.Name(), t.InfoHash().HexString(), spec.InfoHash.HexString(),
+				)
+			}
 		}
 
 		t, _ = s.c.AddTorrentOpt(torrent.AddTorrentOpts{
@@ -149,9 +168,67 @@ func (s *Service) AddTorrent(ctx context.Context, f vfs.File) (*torrent.Torrent,
 	return t, nil
 }
 
-func isTorrentCompatable(existingInfo, newInfo metainfo.Info) bool {
-	existingFiles := slices.Clone(existingInfo.Files)
-	newFiles := slices.Clone(newInfo.Files)
+func (s *Service) checkTorrentCompatable(ctx context.Context, ih infohash.T, info metainfo.Info) (compatable bool, tryLater bool, err error) {
+	log := s.log.With("new-name", info.BestName(), "new-infohash", ih.String())
+
+	name := info.BestName()
+
+	aq, found, err := s.dirsAquire.Get(ctx, info.BestName())
+	if err != nil {
+		return false, false, err
+	}
+	if !found {
+		err = s.dirsAquire.Set(ctx, name, DirAquire{
+			Name:   name,
+			Hashes: slices.Compact([]infohash.T{ih}),
+		})
+		if err != nil {
+			return false, false, err
+		}
+
+		log.Debug("acquiring was not found, so created")
+		return true, false, nil
+	}
+
+	if slices.Contains(aq.Hashes, ih) {
+		log.Debug("hash already know to be compatable")
+		return true, false, nil
+	}
+
+	for _, existingTorrent := range s.c.Torrents() {
+		if existingTorrent.Name() != name || existingTorrent.InfoHash() == ih {
+			continue
+		}
+
+		existingInfo := existingTorrent.Info()
+
+		existingFiles := slices.Clone(existingInfo.Files)
+		newFiles := slices.Clone(info.Files)
+
+		if !s.checkTorrentFilesCompatable(aq, existingFiles, newFiles) {
+			return false, false, nil
+		}
+
+		aq.Hashes = slicesUnique(append(aq.Hashes, ih))
+		err = s.dirsAquire.Set(ctx, aq.Name, aq)
+		if err != nil {
+			log.Warn("torrent not compatible")
+			return false, false, err
+		}
+
+	}
+
+	if slices.Contains(aq.Hashes, ih) {
+		log.Debug("hash is compatable")
+		return true, false, nil
+	}
+
+	log.Debug("torrent with same name not found, try later")
+	return false, true, nil
+}
+
+func (s *Service) checkTorrentFilesCompatable(aq DirAquire, existingFiles, newFiles []metainfo.FileInfo) bool {
+	log := s.log.With("name", aq.Name)
 
 	pathCmp := func(a, b metainfo.FileInfo) int {
 		return slices.Compare(a.BestPath(), b.BestPath())
@@ -167,14 +244,45 @@ func isTorrentCompatable(existingInfo, newInfo metainfo.Info) bool {
 	}
 
 	if len(newFiles) > len(existingFiles) {
-		all := append(existingFiles, newFiles...)
-		slices.SortStableFunc(all, pathCmp)
-		slices.CompactFunc(all, func(fi1, fi2 metainfo.FileInfo) bool {
-			return slices.Equal(fi1.BestPath(), fi2.BestPath()) && fi1.Length == fi2.Length
-		})
+		type fileInfo struct {
+			Path   string
+			Length int64
+		}
+		mapInfo := func(fi metainfo.FileInfo) fileInfo {
+			return fileInfo{
+				Path:   strings.Join(fi.BestPath(), "/"),
+				Length: fi.Length,
+			}
+		}
+
+		existingFiles := apply(existingFiles, mapInfo)
+		newFiles := apply(newFiles, mapInfo)
+
+		for _, n := range newFiles {
+			if slices.Contains(existingFiles, n) {
+				continue
+			}
+
+			for _, e := range existingFiles {
+				if e.Path == n.Path && e.Length != n.Length {
+					log.Warn("torrents not compatible, has files with different length", "path", n.Path, "existing-length", e.Length, "new-length", e.Length)
+					return false
+				}
+			}
+		}
 	}
 
-	return false
+	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 {
@@ -188,12 +296,17 @@ func (s *Service) NewTorrentFs(f vfs.File) (vfs.Filesystem, error) {
 	defer cancel()
 	defer f.Close()
 
+	info, err := f.Stat()
+	if err != nil {
+		return nil, err
+	}
+
 	t, err := s.AddTorrent(ctx, f)
 	if err != nil {
 		return nil, err
 	}
 
-	return vfs.NewTorrentFs(controller.NewTorrent(t, s.excludedFiles), s.readTimeout), nil
+	return vfs.NewTorrentFs(info.Name(), controller.NewTorrent(t, s.excludedFiles), s.readTimeout), nil
 }
 
 func (s *Service) Stats() (*Stats, error) {
@@ -252,3 +365,20 @@ func (s *Service) GetTorrent(infohashHex string) (*controller.Torrent, error) {
 
 	return controller.NewTorrent(t, s.excludedFiles), nil
 }
+
+func slicesUnique[S ~[]E, E comparable](in S) S {
+	m := map[E]struct{}{}
+	for _, v := range in {
+		m[v] = struct{}{}
+	}
+
+	return maps.Keys(m)
+}
+
+func apply[I, O any](in []I, f func(e I) O) []O {
+	out := []O{}
+	for _, v := range in {
+		out = append(out, f(v))
+	}
+	return out
+}
diff --git a/src/host/vfs/archive.go b/src/host/vfs/archive.go
index d0708fa..3e28bfd 100644
--- a/src/host/vfs/archive.go
+++ b/src/host/vfs/archive.go
@@ -40,9 +40,9 @@ var ArchiveFactories = map[string]FsFactory{
 
 type archiveLoader func(r iio.Reader, size int64) (map[string]*archiveFile, error)
 
-var _ Filesystem = &archive{}
+var _ Filesystem = &ArchiveFS{}
 
-type archive struct {
+type ArchiveFS struct {
 	name string
 
 	r iio.Reader
@@ -52,8 +52,8 @@ type archive struct {
 	files func() (map[string]File, error)
 }
 
-func NewArchive(name string, r iio.Reader, size int64, loader archiveLoader) *archive {
-	return &archive{
+func NewArchive(name string, r iio.Reader, size int64, loader archiveLoader) *ArchiveFS {
+	return &ArchiveFS{
 		name: name,
 		r:    r,
 		size: size,
@@ -94,11 +94,11 @@ func NewArchive(name string, r iio.Reader, size int64, loader archiveLoader) *ar
 }
 
 // Unlink implements Filesystem.
-func (a *archive) Unlink(filename string) error {
+func (a *ArchiveFS) Unlink(filename string) error {
 	return ErrNotImplemented
 }
 
-func (a *archive) Open(filename string) (File, error) {
+func (a *ArchiveFS) Open(filename string) (File, error) {
 	files, err := a.files()
 	if err != nil {
 		return nil, err
@@ -107,7 +107,7 @@ func (a *archive) Open(filename string) (File, error) {
 	return getFile(files, filename)
 }
 
-func (fs *archive) ReadDir(path string) ([]fs.DirEntry, error) {
+func (fs *ArchiveFS) ReadDir(path string) ([]fs.DirEntry, error) {
 	files, err := fs.files()
 	if err != nil {
 		return nil, err
@@ -117,7 +117,7 @@ func (fs *archive) ReadDir(path string) ([]fs.DirEntry, error) {
 }
 
 // Stat implements Filesystem.
-func (afs *archive) Stat(filename string) (fs.FileInfo, error) {
+func (afs *ArchiveFS) Stat(filename string) (fs.FileInfo, error) {
 	files, err := afs.files()
 	if err != nil {
 		return nil, err
diff --git a/src/host/vfs/resolver.go b/src/host/vfs/resolver.go
index a51957f..79ada38 100644
--- a/src/host/vfs/resolver.go
+++ b/src/host/vfs/resolver.go
@@ -51,7 +51,21 @@ func (r *ResolveFS) ReadDir(dir string) ([]fs.DirEntry, error) {
 	out := make([]fs.DirEntry, 0, len(entries))
 	for _, e := range entries {
 		if r.resolver.isNestedFs(e.Name()) {
-			out = append(out, newDirInfo(e.Name()))
+			filepath := path.Join(dir, e.Name())
+			file, err := r.Open(filepath)
+			if err != nil {
+				return nil, err
+			}
+			nfs, err := r.resolver.nestedFs(filepath, file)
+			if err != nil {
+				return nil, err
+			}
+
+			if e, ok := nfs.(fs.DirEntry); ok {
+				out = append(out, e)
+			} else {
+				out = append(out, newDirInfo(e.Name()))
+			}
 		} else {
 			out = append(out, e)
 		}
diff --git a/src/host/vfs/resolver_test.go b/src/host/vfs/resolver_test.go
index aa6882e..01eb77a 100644
--- a/src/host/vfs/resolver_test.go
+++ b/src/host/vfs/resolver_test.go
@@ -83,7 +83,7 @@ func TestResolver(t *testing.T) {
 		require.NoError(err)
 		require.Equal("/f1.rar", fsPath)
 		require.Equal("/f2.rar", nestedFsPath)
-		require.IsType(&archive{}, nestedFs)
+		require.IsType(&ArchiveFS{}, nestedFs)
 	})
 	t.Run("root", func(t *testing.T) {
 		t.Parallel()
@@ -123,7 +123,7 @@ func TestResolver(t *testing.T) {
 		require.NoError(err)
 		require.Equal("/f1.rar", fsPath)
 		require.Equal("/", nestedFsPath)
-		require.IsType(&archive{}, nestedFs)
+		require.IsType(&ArchiveFS{}, nestedFs)
 	})
 	t.Run("inside folder", func(t *testing.T) {
 		t.Parallel()
@@ -134,7 +134,7 @@ func TestResolver(t *testing.T) {
 			return &Dummy{}, nil
 		})
 		require.NoError(err)
-		require.IsType(&archive{}, nestedFs)
+		require.IsType(&ArchiveFS{}, nestedFs)
 		require.Equal("/test1/f1.rar", fsPath)
 		require.Equal("/", nestedFsPath)
 	})
diff --git a/src/host/vfs/torrent.go b/src/host/vfs/torrent.go
index 0bbe3b6..4b51e9d 100644
--- a/src/host/vfs/torrent.go
+++ b/src/host/vfs/torrent.go
@@ -21,8 +21,10 @@ import (
 var _ Filesystem = &TorrentFs{}
 
 type TorrentFs struct {
-	mu sync.Mutex
-	c  *controller.Torrent
+	name string
+
+	mu      sync.Mutex
+	Torrent *controller.Torrent
 
 	readTimeout int
 
@@ -31,14 +33,37 @@ type TorrentFs struct {
 	resolver *resolver
 }
 
-func NewTorrentFs(c *controller.Torrent, readTimeout int) *TorrentFs {
+func NewTorrentFs(name string, c *controller.Torrent, readTimeout int) *TorrentFs {
 	return &TorrentFs{
-		c:           c,
+		name:        name,
+		Torrent:     c,
 		readTimeout: readTimeout,
 		resolver:    newResolver(ArchiveFactories),
 	}
 }
 
+var _ fs.DirEntry = (*TorrentFs)(nil)
+
+// Name implements fs.DirEntry.
+func (tfs *TorrentFs) Name() string {
+	return tfs.name
+}
+
+// Info implements fs.DirEntry.
+func (tfs *TorrentFs) Info() (fs.FileInfo, error) {
+	return newDirInfo(tfs.name), nil
+}
+
+// IsDir implements fs.DirEntry.
+func (tfs *TorrentFs) IsDir() bool {
+	return true
+}
+
+// Type implements fs.DirEntry.
+func (tfs *TorrentFs) Type() fs.FileMode {
+	return fs.ModeDir
+}
+
 func (fs *TorrentFs) files() (map[string]File, error) {
 	fs.mu.Lock()
 	defer fs.mu.Unlock()
@@ -47,7 +72,7 @@ func (fs *TorrentFs) files() (map[string]File, error) {
 		return fs.filesCache, nil
 	}
 
-	files, err := fs.c.Files(context.Background())
+	files, err := fs.Torrent.Files(context.Background())
 	if err != nil {
 		return nil, err
 	}
@@ -65,8 +90,8 @@ func (fs *TorrentFs) files() (map[string]File, error) {
 	}
 
 	// TODO optional
-	if len(fs.filesCache) == 1 && fs.resolver.isNestedFs(fs.c.Name()) {
-		filepath := "/" + fs.c.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(filepath, file)
 			if err != nil {
@@ -86,7 +111,7 @@ func (fs *TorrentFs) files() (map[string]File, error) {
 	}
 
 DEFAULT_DIR:
-	rootDir := "/" + fs.c.Name() + "/"
+	rootDir := "/" + fs.Torrent.Name() + "/"
 	singleDir := true
 	for k, _ := range fs.filesCache {
 		if !strings.HasPrefix(k, rootDir) {
@@ -238,7 +263,7 @@ func (fs *TorrentFs) Unlink(name string) error {
 		return ErrNotImplemented
 	}
 
-	return fs.c.ExcludeFile(context.Background(), tfile.file)
+	return fs.Torrent.ExcludeFile(context.Background(), tfile.file)
 }
 
 type reader interface {
diff --git a/src/log/log.go b/src/log/log.go
index 654de65..6d0bcdb 100644
--- a/src/log/log.go
+++ b/src/log/log.go
@@ -1,30 +1,21 @@
 package log
 
-import (
-	"log/slog"
-	"os"
-	"time"
-
-	"git.kmsign.ru/royalcat/tstor/src/config"
-	"github.com/lmittmann/tint"
-)
-
 const FileName = "tstor.log"
 
-func Load(config *config.Log) {
-	level := slog.LevelInfo
-	if config.Debug {
-		level = slog.LevelDebug
-	}
+// func Load(config *config.Log) {
+// 	level := slog.LevelInfo
+// 	if config.Debug {
+// 		level = slog.LevelDebug
+// 	}
 
-	slog.SetDefault(slog.New(
-		tint.NewHandler(os.Stdout, &tint.Options{
-			Level:      level,
-			TimeFormat: time.Kitchen,
-			// NoColor:    !isatty.IsTerminal(os.Stdout.Fd()),
-		}),
-	))
-}
+// 	slog.SetDefault(slog.New(
+// 		tint.NewHandler(os.Stdout, &tint.Options{
+// 			Level:      level,
+// 			TimeFormat: time.Kitchen,
+// 			// NoColor:    !isatty.IsTerminal(os.Stdout.Fd()),
+// 		}),
+// 	))
+// }
 
 // func newRollingFile(config *config.Log) io.Writer {
 // 	if err := os.MkdirAll(config.Path, 0744); err != nil {
diff --git a/src/telemetry/setup.go b/src/telemetry/setup.go
new file mode 100644
index 0000000..5e86ba0
--- /dev/null
+++ b/src/telemetry/setup.go
@@ -0,0 +1,126 @@
+package telemetry
+
+import (
+	"context"
+	"log/slog"
+	"os"
+
+	"git.kmsign.ru/royalcat/tstor/pkg/rlog"
+	"github.com/agoda-com/opentelemetry-go/otelslog"
+	"github.com/agoda-com/opentelemetry-logs-go/exporters/otlp/otlplogs"
+	"github.com/agoda-com/opentelemetry-logs-go/exporters/otlp/otlplogs/otlplogshttp"
+	logsdk "github.com/agoda-com/opentelemetry-logs-go/sdk/logs"
+	"go.opentelemetry.io/otel"
+	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
+	"go.opentelemetry.io/otel/exporters/prometheus"
+	"go.opentelemetry.io/otel/sdk/metric"
+	"go.opentelemetry.io/otel/sdk/resource"
+	"go.opentelemetry.io/otel/sdk/trace"
+	semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
+)
+
+type Client struct {
+	log *slog.Logger
+
+	tracerProvider *trace.TracerProvider
+	metricProvider *metric.MeterProvider
+	loggerProvider *logsdk.LoggerProvider
+}
+
+func (client *Client) Shutdown(ctx context.Context) {
+	log := rlog.FunctionLog(client.log, "Shutdown")
+	if client.metricProvider == nil {
+		err := client.metricProvider.Shutdown(ctx)
+		if err != nil {
+			log.Error("error shutting down metric provider", rlog.Err(err))
+		}
+	}
+	if client.tracerProvider == nil {
+		err := client.tracerProvider.Shutdown(ctx)
+		if err != nil {
+			log.Error("error shutting down tracer provider", rlog.Err(err))
+		}
+	}
+	if client.loggerProvider == nil {
+		err := client.loggerProvider.Shutdown(ctx)
+		if err != nil {
+			log.Error("error shutting down logger provider", rlog.Err(err))
+		}
+	}
+}
+
+const appName = "tstor"
+
+func Setup(ctx context.Context, endpoint string) (*Client, error) {
+	log := rlog.ComponentLog("telemetry")
+
+	client := &Client{
+		log: log,
+	}
+	otel.SetErrorHandler(otel.ErrorHandlerFunc(func(cause error) {
+		log.Error("otel error", rlog.Err(cause))
+	}))
+
+	hostName, _ := os.Hostname()
+
+	r, err := resource.Merge(
+		resource.Default(),
+		resource.NewWithAttributes(
+			semconv.SchemaURL,
+			semconv.ServiceName(appName),
+			semconv.HostName(hostName),
+		),
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	metricExporter, err := prometheus.New(prometheus.WithNamespace(appName))
+	if err != nil {
+		return nil, err
+	}
+	client.metricProvider = metric.NewMeterProvider(
+		metric.WithReader(metricExporter),
+		metric.WithResource(r),
+	)
+	otel.SetMeterProvider(client.metricProvider)
+	log.Info("prometheus metrics provider initialized")
+
+	traceExporter, err := otlptracehttp.New(ctx,
+		otlptracehttp.WithEndpoint(endpoint),
+		otlptracehttp.WithRetry(otlptracehttp.RetryConfig{
+			Enabled: false,
+		}),
+	)
+	if err != nil {
+		return nil, err
+	}
+	client.tracerProvider = trace.NewTracerProvider(
+		trace.WithBatcher(traceExporter),
+		trace.WithResource(r),
+	)
+	otel.SetTracerProvider(client.tracerProvider)
+	log.Info("otel tracing provider initialized")
+
+	logExporter, err := otlplogs.NewExporter(ctx,
+		otlplogs.WithClient(
+			otlplogshttp.NewClient(otlplogshttp.WithEndpoint(endpoint)),
+		),
+	)
+	if err != nil {
+		return nil, err
+	}
+	client.loggerProvider = logsdk.NewLoggerProvider(
+		logsdk.WithBatcher(logExporter),
+		logsdk.WithResource(r),
+	)
+
+	rlog.AddHandler(otelslog.NewOtelHandler(client.loggerProvider,
+		&otelslog.HandlerOptions{
+			Level: slog.LevelDebug,
+		}),
+	)
+	client.log = slog.Default()
+
+	return client, nil
+}

From fd3beea87450e3d664afb806b16ee8c9db60e8af Mon Sep 17 00:00:00 2001
From: royalcat <k.adamovich20@gmail.com>
Date: Wed, 20 Mar 2024 13:49:19 +0300
Subject: [PATCH 15/18] fs is dir entry

---
 src/config/load.go                            | 11 ++++-
 .../graphql/resolver/query.resolvers.go       | 11 +++++
 src/export/fuse/mount_test.go                 |  4 +-
 src/export/webdav/fs_test.go                  | 10 ++---
 src/host/vfs/archive.go                       | 28 +++++++++++-
 src/host/vfs/fs.go                            |  2 +
 src/host/vfs/log.go                           | 20 +++++++++
 src/host/vfs/memory.go                        | 24 +++++++++-
 src/host/vfs/memory_test.go                   |  2 +-
 src/host/vfs/os.go                            | 20 +++++++++
 src/host/vfs/resolver.go                      | 44 +++++++++++++------
 src/host/vfs/resolver_test.go                 | 23 +++++++++-
 12 files changed, 172 insertions(+), 27 deletions(-)

diff --git a/src/config/load.go b/src/config/load.go
index 9f7a78e..2694baa 100644
--- a/src/config/load.go
+++ b/src/config/load.go
@@ -21,7 +21,16 @@ func Load(path string) (*Config, error) {
 	}
 
 	if path != "" {
-		_ = k.Load(file.Provider(path), yaml.Parser()) // its ok if file doesnt exist
+		_, err := os.Stat(path)
+		if err != nil && !os.IsNotExist(err) { // its ok if file doesnt exist
+			return nil, err
+
+		}
+
+		err = k.Load(file.Provider(path), yaml.Parser())
+		if err != nil {
+			return nil, err
+		}
 	}
 
 	err = k.Load(env.Provider("TSTOR_", ".", func(s string) string {
diff --git a/src/delivery/graphql/resolver/query.resolvers.go b/src/delivery/graphql/resolver/query.resolvers.go
index 30ee852..42a2726 100644
--- a/src/delivery/graphql/resolver/query.resolvers.go
+++ b/src/delivery/graphql/resolver/query.resolvers.go
@@ -73,6 +73,17 @@ func (r *queryResolver) FsListDir(ctx context.Context, path string) ([]model.Dir
 	out := []model.DirEntry{}
 	for _, e := range entries {
 		switch e.(type) {
+		case *vfs.ArchiveFS:
+			e := e.(*vfs.ArchiveFS)
+			out = append(out, model.ArchiveFs{
+				Name: e.Name(),
+				Size: e.Size,
+			})
+		case *vfs.ResolverFS:
+			e := e.(*vfs.ResolverFS)
+			out = append(out, model.ResolverFs{
+				Name: e.Name(),
+			})
 		case *vfs.TorrentFs:
 			e := e.(*vfs.TorrentFs)
 			out = append(out, model.TorrentFs{
diff --git a/src/export/fuse/mount_test.go b/src/export/fuse/mount_test.go
index 5a973af..8280bb1 100644
--- a/src/export/fuse/mount_test.go
+++ b/src/export/fuse/mount_test.go
@@ -24,7 +24,7 @@ func TestHandler(t *testing.T) {
 
 	h := NewHandler(false, p)
 
-	mem := vfs.NewMemoryFS(map[string]*vfs.MemoryFile{
+	mem := vfs.NewMemoryFS("/", map[string]*vfs.MemoryFile{
 		"/test.txt": vfs.NewMemoryFile("test.txt", []byte("test")),
 	})
 
@@ -51,7 +51,7 @@ func TestHandlerDriveLetter(t *testing.T) {
 
 	h := NewHandler(false, p)
 
-	mem := vfs.NewMemoryFS(map[string]*vfs.MemoryFile{
+	mem := vfs.NewMemoryFS("/", map[string]*vfs.MemoryFile{
 		"/test.txt": vfs.NewMemoryFile("test.txt", []byte("test")),
 	})
 
diff --git a/src/export/webdav/fs_test.go b/src/export/webdav/fs_test.go
index 606696f..a43a50e 100644
--- a/src/export/webdav/fs_test.go
+++ b/src/export/webdav/fs_test.go
@@ -17,7 +17,7 @@ func TestWebDAVFilesystem(t *testing.T) {
 
 	require := require.New(t)
 
-	mfs := vfs.NewMemoryFS(map[string]*vfs.MemoryFile{
+	mfs := vfs.NewMemoryFS("/", map[string]*vfs.MemoryFile{
 		"/folder/file.txt": vfs.NewMemoryFile("file.txt", []byte("test file content.")),
 	})
 
@@ -57,7 +57,7 @@ func TestWebDAVFilesystem(t *testing.T) {
 
 	fInfo, err := wfs.Stat(context.Background(), "/folder/file.txt")
 	require.NoError(err)
-	require.Equal("/folder/file.txt", fInfo.Name())
+	require.Equal("file.txt", fInfo.Name())
 	require.False(fInfo.IsDir())
 	require.Equal(int64(18), fInfo.Size())
 }
@@ -67,13 +67,13 @@ func TestErrNotImplemented(t *testing.T) {
 
 	require := require.New(t)
 
-	mfs := vfs.NewMemoryFS(map[string]*vfs.MemoryFile{
+	mfs := vfs.NewMemoryFS("/", map[string]*vfs.MemoryFile{
 		"/folder/file.txt": vfs.NewMemoryFile("file.txt", []byte("test file content.")),
 	})
 
 	wfs := newFS(mfs)
 
 	require.ErrorIs(wfs.Mkdir(context.Background(), "test", 0), webdav.ErrNotImplemented)
-	require.ErrorIs(wfs.RemoveAll(context.Background(), "test"), webdav.ErrNotImplemented)
-	require.ErrorIs(wfs.Rename(context.Background(), "test", "newTest"), webdav.ErrNotImplemented)
+	// require.ErrorIs(wfs.RemoveAll(context.Background(), "test"), webdav.ErrNotImplemented)
+	// require.ErrorIs(wfs.Rename(context.Background(), "test", "newTest"), webdav.ErrNotImplemented)
 }
diff --git a/src/host/vfs/archive.go b/src/host/vfs/archive.go
index 3e28bfd..b7bc60c 100644
--- a/src/host/vfs/archive.go
+++ b/src/host/vfs/archive.go
@@ -47,7 +47,7 @@ type ArchiveFS struct {
 
 	r iio.Reader
 
-	size int64
+	Size int64
 
 	files func() (map[string]File, error)
 }
@@ -56,7 +56,7 @@ func NewArchive(name string, r iio.Reader, size int64, loader archiveLoader) *Ar
 	return &ArchiveFS{
 		name: name,
 		r:    r,
-		size: size,
+		Size: size,
 		files: OnceValueWOErr(func() (map[string]File, error) {
 			zipFiles, err := loader(r, size)
 			if err != nil {
@@ -136,6 +136,30 @@ func (afs *ArchiveFS) Stat(filename string) (fs.FileInfo, error) {
 	return nil, ErrNotExist
 }
 
+// Info implements Filesystem.
+func (a *ArchiveFS) Info() (fs.FileInfo, error) {
+	return &fileInfo{
+		name:  a.name,
+		size:  a.Size,
+		isDir: true,
+	}, nil
+}
+
+// IsDir implements Filesystem.
+func (a *ArchiveFS) IsDir() bool {
+	return true
+}
+
+// Name implements Filesystem.
+func (a *ArchiveFS) Name() string {
+	return a.name
+}
+
+// Type implements Filesystem.
+func (a *ArchiveFS) Type() fs.FileMode {
+	return fs.ModeDir
+}
+
 var _ File = &archiveFile{}
 
 func NewArchiveFile(name string, readerFunc func() (iio.Reader, error), size int64) *archiveFile {
diff --git a/src/host/vfs/fs.go b/src/host/vfs/fs.go
index 5f1951b..08282d3 100644
--- a/src/host/vfs/fs.go
+++ b/src/host/vfs/fs.go
@@ -31,6 +31,8 @@ type Filesystem interface {
 
 	Stat(filename string) (fs.FileInfo, error)
 	Unlink(filename string) error
+
+	fs.DirEntry
 }
 
 const defaultMode = fs.FileMode(0555)
diff --git a/src/host/vfs/log.go b/src/host/vfs/log.go
index 0fb699d..de85be3 100644
--- a/src/host/vfs/log.go
+++ b/src/host/vfs/log.go
@@ -19,6 +19,26 @@ func WrapLogFS(fs Filesystem, log *slog.Logger) *LogFS {
 	}
 }
 
+// Info implements Filesystem.
+func (fs *LogFS) Info() (fs.FileInfo, error) {
+	return fs.fs.Info()
+}
+
+// IsDir implements Filesystem.
+func (fs *LogFS) IsDir() bool {
+	return fs.fs.IsDir()
+}
+
+// Name implements Filesystem.
+func (fs *LogFS) Name() string {
+	return fs.fs.Name()
+}
+
+// Type implements Filesystem.
+func (fs *LogFS) Type() fs.FileMode {
+	return fs.fs.Type()
+}
+
 // Open implements Filesystem.
 func (fs *LogFS) Open(filename string) (File, error) {
 	file, err := fs.fs.Open(filename)
diff --git a/src/host/vfs/memory.go b/src/host/vfs/memory.go
index 2ef42fb..cd0680a 100644
--- a/src/host/vfs/memory.go
+++ b/src/host/vfs/memory.go
@@ -9,16 +9,38 @@ import (
 var _ Filesystem = &MemoryFs{}
 
 type MemoryFs struct {
+	name  string
 	files map[string]*MemoryFile
 }
 
+// Info implements Filesystem.
+func (fs *MemoryFs) Info() (fs.FileInfo, error) {
+	return newDirInfo(fs.name), nil
+}
+
+// IsDir implements Filesystem.
+func (fs *MemoryFs) IsDir() bool {
+	return true
+}
+
+// Name implements Filesystem.
+func (fs *MemoryFs) Name() string {
+	return fs.name
+}
+
+// Type implements Filesystem.
+func (mfs *MemoryFs) Type() fs.FileMode {
+	return fs.ModeDir
+}
+
 // Unlink implements Filesystem.
 func (fs *MemoryFs) Unlink(filename string) error {
 	return ErrNotImplemented
 }
 
-func NewMemoryFS(files map[string]*MemoryFile) *MemoryFs {
+func NewMemoryFS(name string, files map[string]*MemoryFile) *MemoryFs {
 	return &MemoryFs{
+		name:  name,
 		files: files,
 	}
 }
diff --git a/src/host/vfs/memory_test.go b/src/host/vfs/memory_test.go
index 224c8ef..a174921 100644
--- a/src/host/vfs/memory_test.go
+++ b/src/host/vfs/memory_test.go
@@ -12,7 +12,7 @@ func TestMemory(t *testing.T) {
 	require := require.New(t)
 	testData := "Hello"
 
-	c := NewMemoryFS(map[string]*MemoryFile{
+	c := NewMemoryFS("/", map[string]*MemoryFile{
 		"/dir/here": NewMemoryFile("here", []byte(testData)),
 	})
 
diff --git a/src/host/vfs/os.go b/src/host/vfs/os.go
index add84f3..9b0277a 100644
--- a/src/host/vfs/os.go
+++ b/src/host/vfs/os.go
@@ -39,6 +39,26 @@ func (o *OsFS) ReadDir(dir string) ([]fs.DirEntry, error) {
 	return os.ReadDir(path.Join(o.hostDir, dir))
 }
 
+// Info implements Filesystem.
+func (fs *OsFS) Info() (fs.FileInfo, error) {
+	return newDirInfo(path.Base(fs.hostDir)), nil
+}
+
+// IsDir implements Filesystem.
+func (fs *OsFS) IsDir() bool {
+	return true
+}
+
+// Name implements Filesystem.
+func (fs *OsFS) Name() string {
+	return path.Base(fs.hostDir)
+}
+
+// Type implements Filesystem.
+func (ofs *OsFS) Type() fs.FileMode {
+	return fs.ModeDir
+}
+
 func NewOsFs(osDir string) *OsFS {
 	return &OsFS{
 		hostDir: osDir,
diff --git a/src/host/vfs/resolver.go b/src/host/vfs/resolver.go
index 79ada38..b465b7c 100644
--- a/src/host/vfs/resolver.go
+++ b/src/host/vfs/resolver.go
@@ -9,20 +9,20 @@ import (
 	"sync"
 )
 
-type ResolveFS struct {
+type ResolverFS struct {
 	rootFS   Filesystem
 	resolver *resolver
 }
 
-func NewResolveFS(rootFs Filesystem, factories map[string]FsFactory) *ResolveFS {
-	return &ResolveFS{
+func NewResolveFS(rootFs Filesystem, factories map[string]FsFactory) *ResolverFS {
+	return &ResolverFS{
 		rootFS:   rootFs,
 		resolver: newResolver(factories),
 	}
 }
 
 // Open implements Filesystem.
-func (r *ResolveFS) Open(filename string) (File, error) {
+func (r *ResolverFS) Open(filename string) (File, error) {
 	fsPath, nestedFs, nestedFsPath, err := r.resolver.resolvePath(filename, r.rootFS.Open)
 	if err != nil {
 		return nil, err
@@ -35,7 +35,7 @@ func (r *ResolveFS) Open(filename string) (File, error) {
 }
 
 // ReadDir implements Filesystem.
-func (r *ResolveFS) ReadDir(dir string) ([]fs.DirEntry, error) {
+func (r *ResolverFS) ReadDir(dir string) ([]fs.DirEntry, error) {
 	fsPath, nestedFs, nestedFsPath, err := r.resolver.resolvePath(dir, r.rootFS.Open)
 	if err != nil {
 		return nil, err
@@ -56,16 +56,12 @@ func (r *ResolveFS) ReadDir(dir string) ([]fs.DirEntry, error) {
 			if err != nil {
 				return nil, err
 			}
-			nfs, err := r.resolver.nestedFs(filepath, file)
+			nestedfs, err := r.resolver.nestedFs(filepath, file)
 			if err != nil {
 				return nil, err
 			}
 
-			if e, ok := nfs.(fs.DirEntry); ok {
-				out = append(out, e)
-			} else {
-				out = append(out, newDirInfo(e.Name()))
-			}
+			out = append(out, nestedfs)
 		} else {
 			out = append(out, e)
 		}
@@ -74,7 +70,7 @@ func (r *ResolveFS) ReadDir(dir string) ([]fs.DirEntry, error) {
 }
 
 // Stat implements Filesystem.
-func (r *ResolveFS) Stat(filename string) (fs.FileInfo, error) {
+func (r *ResolverFS) Stat(filename string) (fs.FileInfo, error) {
 	fsPath, nestedFs, nestedFsPath, err := r.resolver.resolvePath(filename, r.rootFS.Open)
 	if err != nil {
 		return nil, err
@@ -87,7 +83,7 @@ func (r *ResolveFS) Stat(filename string) (fs.FileInfo, error) {
 }
 
 // Unlink implements Filesystem.
-func (r *ResolveFS) Unlink(filename string) error {
+func (r *ResolverFS) Unlink(filename string) error {
 	fsPath, nestedFs, nestedFsPath, err := r.resolver.resolvePath(filename, r.rootFS.Open)
 	if err != nil {
 		return err
@@ -99,7 +95,27 @@ func (r *ResolveFS) Unlink(filename string) error {
 	return r.rootFS.Unlink(fsPath)
 }
 
-var _ Filesystem = &ResolveFS{}
+// Info implements Filesystem.
+func (r *ResolverFS) Info() (fs.FileInfo, error) {
+	return newDirInfo(r.rootFS.Name()), nil
+}
+
+// IsDir implements Filesystem.
+func (r *ResolverFS) IsDir() bool {
+	return true
+}
+
+// Name implements Filesystem.
+func (r *ResolverFS) Name() string {
+	return r.Name()
+}
+
+// Type implements Filesystem.
+func (r *ResolverFS) Type() fs.FileMode {
+	return fs.ModeDir
+}
+
+var _ Filesystem = &ResolverFS{}
 
 type FsFactory func(f File) (Filesystem, error)
 
diff --git a/src/host/vfs/resolver_test.go b/src/host/vfs/resolver_test.go
index 01eb77a..c0cef9a 100644
--- a/src/host/vfs/resolver_test.go
+++ b/src/host/vfs/resolver_test.go
@@ -41,6 +41,7 @@ func (d *Dummy) ReadAt(p []byte, off int64) (n int, err error) {
 var _ File = &Dummy{}
 
 type DummyFs struct {
+	name string
 }
 
 // Stat implements Filesystem.
@@ -67,6 +68,26 @@ func (d *DummyFs) ReadDir(path string) ([]fs.DirEntry, error) {
 	return nil, os.ErrNotExist
 }
 
+// Info implements Filesystem.
+func (d *DummyFs) Info() (fs.FileInfo, error) {
+	return newDirInfo(d.name), nil
+}
+
+// IsDir implements Filesystem.
+func (d *DummyFs) IsDir() bool {
+	return true
+}
+
+// Name implements Filesystem.
+func (d *DummyFs) Name() string {
+	return d.name
+}
+
+// Type implements Filesystem.
+func (d *DummyFs) Type() fs.FileMode {
+	return fs.ModeDir
+}
+
 var _ Filesystem = &DummyFs{}
 
 func TestResolver(t *testing.T) {
@@ -174,7 +195,7 @@ func TestFiles(t *testing.T) {
 	{
 		file, err := getFile(files, "/test")
 		require.NoError(err)
-		require.Equal(&dir{}, file)
+		require.Equal(&dir{name: "test"}, file)
 	}
 	{
 		file, err := getFile(files, "/test/file.txt")

From 7b1863109c3a6b8d60320020cf4b062c335d697c Mon Sep 17 00:00:00 2001
From: royalcat <k.adamovich20@gmail.com>
Date: Thu, 21 Mar 2024 00:47:51 +0300
Subject: [PATCH 16/18] context fs

---
 cmd/tstor/main.go                             |   5 +-
 go.mod                                        |   4 +-
 pkg/ctxio/reader.go                           |  48 ++++
 pkg/ctxio/seeker.go                           | 102 ++++++++
 src/config/default.go                         |   4 +-
 src/config/model.go                           |   4 +-
 .../graphql/resolver/query.resolvers.go       |   2 +-
 src/export/fuse/mount.go                      |   9 +-
 src/export/httpfs/httpfs.go                   |  37 ++-
 src/export/nfs/wrapper-v3.go                  | 107 ++++++--
 src/export/webdav/fs.go                       |  33 ++-
 src/host/service/service.go                   |  22 +-
 src/host/vfs/archive.go                       |  50 ++--
 src/host/vfs/archive_test.go                  |  26 +-
 src/host/vfs/dir.go                           |   7 +-
 src/host/vfs/fs.go                            |  15 +-
 src/host/vfs/log.go                           |  32 +--
 src/host/vfs/memory.go                        |  41 +--
 src/host/vfs/memory_test.go                   |  12 +-
 src/host/vfs/os.go                            |  21 +-
 src/host/vfs/resolver.go                      |  53 ++--
 src/host/vfs/resolver_test.go                 |  35 +--
 src/host/vfs/torrent.go                       | 245 ++++++++----------
 src/host/vfs/torrent_test.go                  |  22 +-
 src/iio/wrapper_test.go                       |   6 +-
 25 files changed, 593 insertions(+), 349 deletions(-)
 create mode 100644 pkg/ctxio/reader.go
 create mode 100644 pkg/ctxio/seeker.go

diff --git a/cmd/tstor/main.go b/cmd/tstor/main.go
index 94979f5..f36f761 100644
--- a/cmd/tstor/main.go
+++ b/cmd/tstor/main.go
@@ -125,7 +125,10 @@ func run(configPath string) error {
 	c.AddDhtNodes(conf.TorrentClient.DHTNodes)
 	defer c.Close()
 
-	ts, err := service.NewService(conf.SourceDir, conf.TorrentClient, c, st, excludedFilesStore, infoBytesStore, conf.TorrentClient.AddTimeout, conf.TorrentClient.ReadTimeout)
+	ts, err := service.NewService(
+		conf.SourceDir, conf.TorrentClient,
+		c, st, excludedFilesStore, infoBytesStore,
+	)
 	if err != nil {
 		return fmt.Errorf("error creating service: %w", err)
 	}
diff --git a/go.mod b/go.mod
index 4d9c47f..e39f509 100644
--- a/go.mod
+++ b/go.mod
@@ -4,7 +4,6 @@ go 1.22.1
 
 require (
 	github.com/99designs/gqlgen v0.17.43
-	github.com/RoaringBitmap/roaring v1.2.3
 	github.com/agoda-com/opentelemetry-go/otelslog v0.1.1
 	github.com/agoda-com/opentelemetry-logs-go v0.3.0
 	github.com/anacrolix/dht/v2 v2.21.1
@@ -43,12 +42,14 @@ require (
 	go.opentelemetry.io/otel/exporters/prometheus v0.46.0
 	go.opentelemetry.io/otel/sdk v1.24.0
 	go.opentelemetry.io/otel/sdk/metric v1.24.0
+	go.opentelemetry.io/otel/trace v1.24.0
 	go.uber.org/multierr v1.11.0
 	golang.org/x/exp v0.0.0-20231226003508-02704c960a9b
 	golang.org/x/net v0.19.0
 )
 
 require (
+	github.com/RoaringBitmap/roaring v1.2.3 // indirect
 	github.com/agnivade/levenshtein v1.1.1 // indirect
 	github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect
 	github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
@@ -162,7 +163,6 @@ require (
 	go.opentelemetry.io/contrib v1.21.1 // indirect
 	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect
 	go.opentelemetry.io/otel/metric v1.24.0 // indirect
-	go.opentelemetry.io/otel/trace v1.24.0 // indirect
 	go.opentelemetry.io/proto/otlp v1.1.0 // indirect
 	go4.org v0.0.0-20200411211856-f5505b9728dd // indirect
 	golang.org/x/arch v0.3.0 // indirect
diff --git a/pkg/ctxio/reader.go b/pkg/ctxio/reader.go
new file mode 100644
index 0000000..2ba9f7d
--- /dev/null
+++ b/pkg/ctxio/reader.go
@@ -0,0 +1,48 @@
+package ctxio
+
+import (
+	"context"
+	"io"
+)
+
+type ReaderAtCloser interface {
+	ReaderAt
+	Closer
+}
+
+type ReaderAt interface {
+	ReadAt(ctx context.Context, p []byte, off int64) (n int, err error)
+}
+
+type Reader interface {
+	Read(ctx context.Context, p []byte) (n int, err error)
+}
+
+type Closer interface {
+	Close(ctx context.Context) error
+}
+type contextReader struct {
+	ctx context.Context
+	r   Reader
+}
+
+func (r *contextReader) Read(p []byte) (n int, err error) {
+	return r.r.Read(r.ctx, p)
+}
+
+func IoReaderAt(ctx context.Context, r ReaderAt) io.ReaderAt {
+	return &contextReaderAt{ctx: ctx, r: r}
+}
+
+type contextReaderAt struct {
+	ctx context.Context
+	r   ReaderAt
+}
+
+func (c *contextReaderAt) ReadAt(p []byte, off int64) (n int, err error) {
+	return c.r.ReadAt(c.ctx, p, off)
+}
+
+func IoReader(ctx context.Context, r Reader) io.Reader {
+	return &contextReader{ctx: ctx, r: r}
+}
diff --git a/pkg/ctxio/seeker.go b/pkg/ctxio/seeker.go
new file mode 100644
index 0000000..5284ecc
--- /dev/null
+++ b/pkg/ctxio/seeker.go
@@ -0,0 +1,102 @@
+package ctxio
+
+import (
+	"context"
+	"io"
+	"sync"
+)
+
+type ioSeekerWrapper struct {
+	ctx context.Context
+
+	mu   sync.Mutex
+	pos  int64
+	size int64
+
+	r ReaderAt
+}
+
+func IoReadSeekerWrapper(ctx context.Context, r ReaderAt, size int64) io.ReadSeeker {
+	return &ioSeekerWrapper{
+		ctx:  ctx,
+		r:    r,
+		size: size,
+	}
+}
+
+func (r *ioSeekerWrapper) Seek(offset int64, whence int) (int64, error) {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+
+	switch whence {
+	case io.SeekStart:
+		r.pos = offset
+	case io.SeekCurrent:
+		r.pos = r.pos + offset
+	case io.SeekEnd:
+		r.pos = r.size + offset
+	}
+
+	return r.pos, nil
+}
+
+func (r *ioSeekerWrapper) Read(p []byte) (int, error) {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+
+	n, err := r.r.ReadAt(r.ctx, p, r.pos)
+	r.pos += int64(n)
+
+	return n, err
+}
+
+var _ io.ReadSeekCloser = (*ioSeekerCloserWrapper)(nil)
+
+type ioSeekerCloserWrapper struct {
+	ctx context.Context
+
+	mu   sync.Mutex
+	pos  int64
+	size int64
+
+	r ReaderAtCloser
+}
+
+func IoReadSeekCloserWrapper(ctx context.Context, r ReaderAtCloser, size int64) io.ReadSeekCloser {
+	return &ioSeekerCloserWrapper{
+		ctx:  ctx,
+		r:    r,
+		size: size,
+	}
+}
+
+func (r *ioSeekerCloserWrapper) Seek(offset int64, whence int) (int64, error) {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+
+	switch whence {
+	case io.SeekStart:
+		r.pos = offset
+	case io.SeekCurrent:
+		r.pos = r.pos + offset
+	case io.SeekEnd:
+		r.pos = r.size + offset
+	}
+
+	return r.pos, nil
+}
+
+func (r *ioSeekerCloserWrapper) Read(p []byte) (int, error) {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+
+	n, err := r.r.ReadAt(r.ctx, p, r.pos)
+	r.pos += int64(n)
+
+	return n, err
+}
+
+// Close implements io.ReadSeekCloser.
+func (r *ioSeekerCloserWrapper) Close() error {
+	return r.r.Close(r.ctx)
+}
diff --git a/src/config/default.go b/src/config/default.go
index 9a3ebcb..e75be06 100644
--- a/src/config/default.go
+++ b/src/config/default.go
@@ -33,8 +33,8 @@ var defaultConfig = Config{
 
 		// GlobalCacheSize: 2048,
 
-		AddTimeout:  60,
-		ReadTimeout: 120,
+		// AddTimeout:  60,
+		// ReadTimeout: 120,
 	},
 
 	Log: Log{
diff --git a/src/config/model.go b/src/config/model.go
index 3aaf665..d8e8c03 100644
--- a/src/config/model.go
+++ b/src/config/model.go
@@ -26,8 +26,8 @@ type Log struct {
 }
 
 type TorrentClient struct {
-	ReadTimeout int `koanf:"read_timeout,omitempty"`
-	AddTimeout  int `koanf:"add_timeout,omitempty"`
+	// ReadTimeout int `koanf:"read_timeout,omitempty"`
+	// AddTimeout  int `koanf:"add_timeout,omitempty"`
 
 	DHTNodes    []string `koanf:"dhtnodes,omitempty"`
 	DisableIPv6 bool     `koanf:"disable_ipv6,omitempty"`
diff --git a/src/delivery/graphql/resolver/query.resolvers.go b/src/delivery/graphql/resolver/query.resolvers.go
index 42a2726..a0a826d 100644
--- a/src/delivery/graphql/resolver/query.resolvers.go
+++ b/src/delivery/graphql/resolver/query.resolvers.go
@@ -66,7 +66,7 @@ func (r *queryResolver) Torrents(ctx context.Context, filter *model.TorrentsFilt
 
 // FsListDir is the resolver for the fsListDir field.
 func (r *queryResolver) FsListDir(ctx context.Context, path string) ([]model.DirEntry, error) {
-	entries, err := r.VFS.ReadDir(path)
+	entries, err := r.VFS.ReadDir(ctx, path)
 	if err != nil {
 		return nil, err
 	}
diff --git a/src/export/fuse/mount.go b/src/export/fuse/mount.go
index e6590be..9248b70 100644
--- a/src/export/fuse/mount.go
+++ b/src/export/fuse/mount.go
@@ -3,6 +3,7 @@
 package fuse
 
 import (
+	"context"
 	"errors"
 	"io"
 	"log/slog"
@@ -104,7 +105,7 @@ func (fs *fuseFS) Read(path string, dest []byte, off int64, fh uint64) int {
 
 	buf := dest[:end]
 
-	n, err := file.ReadAt(buf, off)
+	n, err := file.ReadAt(context.TODO(), buf, off)
 	if err != nil && err != io.EOF {
 		log.Error("error reading data")
 		return -fuse.EIO
@@ -178,7 +179,7 @@ func (fh *fileHandler) ListDir(path string) ([]string, error) {
 	fh.mu.RLock()
 	defer fh.mu.RUnlock()
 
-	files, err := fh.fs.ReadDir(path)
+	files, err := fh.fs.ReadDir(context.TODO(), path)
 	if err != nil {
 		return nil, err
 	}
@@ -237,7 +238,7 @@ func (fh *fileHandler) Remove(fhi uint64) error {
 		return ErrHolderEmpty
 	}
 
-	if err := f.Close(); err != nil {
+	if err := f.Close(context.TODO()); err != nil {
 		return err
 	}
 
@@ -247,7 +248,7 @@ func (fh *fileHandler) Remove(fhi uint64) error {
 }
 
 func (fh *fileHandler) lookupFile(path string) (vfs.File, error) {
-	file, err := fh.fs.Open(path)
+	file, err := fh.fs.Open(context.TODO(), path)
 	if err != nil {
 		return nil, err
 	}
diff --git a/src/export/httpfs/httpfs.go b/src/export/httpfs/httpfs.go
index 303cf2d..8a7c188 100644
--- a/src/export/httpfs/httpfs.go
+++ b/src/export/httpfs/httpfs.go
@@ -1,18 +1,24 @@
 package httpfs
 
 import (
+	"context"
 	"io"
 	"io/fs"
 	"net/http"
 	"os"
 	"sync"
 
+	"git.kmsign.ru/royalcat/tstor/pkg/ctxio"
 	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
-	"git.kmsign.ru/royalcat/tstor/src/iio"
+	"go.opentelemetry.io/otel"
+	"go.opentelemetry.io/otel/attribute"
+	"go.opentelemetry.io/otel/trace"
 )
 
 var _ http.FileSystem = &HTTPFS{}
 
+var httpFsTracer = otel.Tracer("git.kmsign.ru/royalcat/tstor/src/export/httpfs.HTTPFS")
+
 type HTTPFS struct {
 	fs vfs.Filesystem
 }
@@ -21,8 +27,16 @@ func NewHTTPFS(fs vfs.Filesystem) *HTTPFS {
 	return &HTTPFS{fs: fs}
 }
 
+func (fs *HTTPFS) ctx() context.Context {
+	return context.Background()
+}
+
 func (hfs *HTTPFS) Open(name string) (http.File, error) {
-	f, err := hfs.fs.Open(name)
+	ctx, span := httpFsTracer.Start(hfs.ctx(), "Open",
+		trace.WithAttributes(attribute.String("name", name)),
+	)
+	defer span.End()
+	f, err := hfs.fs.Open(ctx, name)
 	if err != nil {
 		return nil, err
 	}
@@ -36,11 +50,16 @@ func (hfs *HTTPFS) Open(name string) (http.File, error) {
 		}
 	}
 
-	return newHTTPFile(f, fis), nil
+	return newHTTPFile(ctx, f, fis), nil
 }
 
 func (hfs *HTTPFS) filesToFileInfo(name string) ([]fs.FileInfo, error) {
-	files, err := hfs.fs.ReadDir(name)
+	ctx, span := httpFsTracer.Start(hfs.ctx(), "Open",
+		trace.WithAttributes(attribute.String("name", name)),
+	)
+	defer span.End()
+
+	files, err := hfs.fs.ReadDir(ctx, name)
 	if err != nil {
 		return nil, err
 	}
@@ -62,7 +81,7 @@ var _ http.File = &httpFile{}
 type httpFile struct {
 	f vfs.File
 
-	iio.ReaderSeeker
+	io.ReadSeekCloser
 
 	mu sync.Mutex
 	// dirPos is protected by mu.
@@ -70,11 +89,11 @@ type httpFile struct {
 	dirContent []os.FileInfo
 }
 
-func newHTTPFile(f vfs.File, dirContent []os.FileInfo) *httpFile {
+func newHTTPFile(ctx context.Context, f vfs.File, dirContent []os.FileInfo) *httpFile {
 	return &httpFile{
-		f:            f,
-		dirContent:   dirContent,
-		ReaderSeeker: iio.NewSeekerWrapper(f, f.Size()),
+		f:              f,
+		dirContent:     dirContent,
+		ReadSeekCloser: ctxio.IoReadSeekCloserWrapper(ctx, f, f.Size()),
 	}
 }
 
diff --git a/src/export/nfs/wrapper-v3.go b/src/export/nfs/wrapper-v3.go
index fc4f6b7..9891deb 100644
--- a/src/export/nfs/wrapper-v3.go
+++ b/src/export/nfs/wrapper-v3.go
@@ -1,6 +1,7 @@
 package nfs
 
 import (
+	"context"
 	"errors"
 	"io/fs"
 	"log/slog"
@@ -8,8 +9,13 @@ import (
 
 	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
 	"github.com/go-git/go-billy/v5"
+	"go.opentelemetry.io/otel"
+	"go.opentelemetry.io/otel/attribute"
+	"go.opentelemetry.io/otel/trace"
 )
 
+var billyFsTracer = otel.Tracer("git.kmsign.ru/royalcat/tstor/src/export/nfs.billyFsWrapper")
+
 type billyFsWrapper struct {
 	fs  vfs.Filesystem
 	log *slog.Logger
@@ -18,6 +24,10 @@ type billyFsWrapper struct {
 var _ billy.Filesystem = (*billyFsWrapper)(nil)
 var _ billy.Dir = (*billyFsWrapper)(nil)
 
+func (*billyFsWrapper) ctx() context.Context {
+	return context.Background()
+}
+
 // Chroot implements billy.Filesystem.
 func (*billyFsWrapper) Chroot(path string) (billy.Filesystem, error) {
 	return nil, billy.ErrNotSupported
@@ -35,9 +45,12 @@ func (*billyFsWrapper) Join(elem ...string) string {
 
 // Lstat implements billy.Filesystem.
 func (fs *billyFsWrapper) Lstat(filename string) (fs.FileInfo, error) {
-	info, err := fs.fs.Stat(filename)
+	ctx, span := billyFsTracer.Start(fs.ctx(), "Lstat", trace.WithAttributes(attribute.String("filename", filename)))
+	defer span.End()
+
+	info, err := fs.fs.Stat(ctx, filename)
 	if err != nil {
-		return nil, billyErr(err, fs.log)
+		return nil, billyErr(ctx, err, fs.log)
 	}
 	return info, nil
 }
@@ -49,9 +62,14 @@ func (*billyFsWrapper) MkdirAll(filename string, perm fs.FileMode) error {
 
 // Open implements billy.Filesystem.
 func (fs *billyFsWrapper) Open(filename string) (billy.File, error) {
-	file, err := fs.fs.Open(filename)
+	ctx, span := billyFsTracer.Start(fs.ctx(), "Open",
+		trace.WithAttributes(attribute.String("filename", filename)),
+	)
+	defer span.End()
+
+	file, err := fs.fs.Open(ctx, filename)
 	if err != nil {
-		return nil, billyErr(err, fs.log)
+		return nil, billyErr(ctx, err, fs.log)
 	}
 	return &billyFile{
 		name: filename,
@@ -62,9 +80,14 @@ func (fs *billyFsWrapper) Open(filename string) (billy.File, error) {
 
 // OpenFile implements billy.Filesystem.
 func (fs *billyFsWrapper) OpenFile(filename string, flag int, perm fs.FileMode) (billy.File, error) {
-	file, err := fs.fs.Open(filename)
+	ctx, span := billyFsTracer.Start(fs.ctx(), "OpenFile",
+		trace.WithAttributes(attribute.String("filename", filename)),
+	)
+	defer span.End()
+
+	file, err := fs.fs.Open(ctx, filename)
 	if err != nil {
-		return nil, billyErr(err, fs.log)
+		return nil, billyErr(ctx, err, fs.log)
 	}
 	return &billyFile{
 		name: filename,
@@ -75,9 +98,14 @@ func (fs *billyFsWrapper) OpenFile(filename string, flag int, perm fs.FileMode)
 
 // ReadDir implements billy.Filesystem.
 func (bfs *billyFsWrapper) ReadDir(path string) ([]fs.FileInfo, error) {
-	ffs, err := bfs.fs.ReadDir(path)
+	ctx, span := billyFsTracer.Start(bfs.ctx(), "OpenFile",
+		trace.WithAttributes(attribute.String("path", path)),
+	)
+	defer span.End()
+
+	ffs, err := bfs.fs.ReadDir(ctx, path)
 	if err != nil {
-		return nil, billyErr(err, bfs.log)
+		return nil, billyErr(ctx, err, bfs.log)
 	}
 
 	out := make([]fs.FileInfo, 0, len(ffs))
@@ -102,8 +130,13 @@ func (*billyFsWrapper) Readlink(link string) (string, error) {
 }
 
 // Remove implements billy.Filesystem.
-func (s *billyFsWrapper) Remove(filename string) error {
-	return s.fs.Unlink(filename)
+func (bfs *billyFsWrapper) Remove(filename string) error {
+	ctx, span := billyFsTracer.Start(bfs.ctx(), "Remove",
+		trace.WithAttributes(attribute.String("filename", filename)),
+	)
+	defer span.End()
+
+	return bfs.fs.Unlink(ctx, filename)
 }
 
 // Rename implements billy.Filesystem.
@@ -117,25 +150,32 @@ func (*billyFsWrapper) Root() string {
 }
 
 // Stat implements billy.Filesystem.
-func (fs *billyFsWrapper) Stat(filename string) (fs.FileInfo, error) {
-	info, err := fs.fs.Stat(filename)
+func (bfs *billyFsWrapper) Stat(filename string) (fs.FileInfo, error) {
+	ctx, span := billyFsTracer.Start(bfs.ctx(), "Remove",
+		trace.WithAttributes(attribute.String("filename", filename)),
+	)
+	defer span.End()
+
+	info, err := bfs.fs.Stat(ctx, filename)
 	if err != nil {
-		return nil, billyErr(err, fs.log)
+		return nil, billyErr(ctx, err, bfs.log)
 	}
 	return info, nil
 }
 
 // Symlink implements billy.Filesystem.
 func (fs *billyFsWrapper) Symlink(target string, link string) error {
-	return billyErr(vfs.ErrNotImplemented, fs.log)
+	return billyErr(nil, vfs.ErrNotImplemented, fs.log)
 }
 
 // TempFile implements billy.Filesystem.
 func (fs *billyFsWrapper) TempFile(dir string, prefix string) (billy.File, error) {
-	return nil, billyErr(vfs.ErrNotImplemented, fs.log)
+	return nil, billyErr(nil, vfs.ErrNotImplemented, fs.log)
 }
 
 type billyFile struct {
+	ctx context.Context
+
 	name string
 	file vfs.File
 	log  *slog.Logger
@@ -154,28 +194,47 @@ func (f *billyFile) Name() string {
 }
 
 // Read implements billy.File.
-func (f *billyFile) Read(p []byte) (n int, err error) {
-	return f.file.Read(p)
+func (bf *billyFile) Read(p []byte) (n int, err error) {
+	ctx, span := billyFsTracer.Start(bf.ctx, "Read",
+		trace.WithAttributes(attribute.Int("length", len(p))),
+	)
+	defer func() {
+		span.SetAttributes(attribute.Int("read", n))
+		span.End()
+	}()
+
+	return bf.file.Read(ctx, p)
 }
 
 // ReadAt implements billy.File.
-func (f *billyFile) ReadAt(p []byte, off int64) (n int, err error) {
-	return f.file.ReadAt(p, off)
+func (bf *billyFile) ReadAt(p []byte, off int64) (n int, err error) {
+	ctx, span := billyFsTracer.Start(bf.ctx, "Read",
+		trace.WithAttributes(
+			attribute.Int("length", len(p)),
+			attribute.Int64("offset", off),
+		),
+	)
+	defer func() {
+		span.SetAttributes(attribute.Int("read", n))
+		span.End()
+	}()
+
+	return bf.file.ReadAt(ctx, p, off)
 }
 
 // Seek implements billy.File.
 func (f *billyFile) Seek(offset int64, whence int) (int64, error) {
-	return 0, billyErr(vfs.ErrNotImplemented, f.log)
+	return 0, billyErr(nil, vfs.ErrNotImplemented, f.log)
 }
 
 // Truncate implements billy.File.
 func (f *billyFile) Truncate(size int64) error {
-	return billyErr(vfs.ErrNotImplemented, f.log)
+	return billyErr(nil, vfs.ErrNotImplemented, f.log)
 }
 
 // Write implements billy.File.
 func (f *billyFile) Write(p []byte) (n int, err error) {
-	return 0, billyErr(vfs.ErrNotImplemented, f.log)
+	return 0, billyErr(nil, vfs.ErrNotImplemented, f.log)
 }
 
 // Lock implements billy.File.
@@ -188,13 +247,13 @@ func (*billyFile) Unlock() error {
 	return nil // TODO
 }
 
-func billyErr(err error, log *slog.Logger) error {
+func billyErr(ctx context.Context, err error, log *slog.Logger) error {
 	if errors.Is(err, vfs.ErrNotImplemented) {
 		return billy.ErrNotSupported
 	}
 	if errors.Is(err, vfs.ErrNotExist) {
 		if err, ok := asErr[*fs.PathError](err); ok {
-			log.Error("file not found", "op", err.Op, "path", err.Path, "error", err.Err)
+			log.ErrorContext(ctx, "file not found", "op", err.Op, "path", err.Path, "error", err.Err)
 		}
 		return fs.ErrNotExist
 	}
diff --git a/src/export/webdav/fs.go b/src/export/webdav/fs.go
index e6411ab..1c8fc83 100644
--- a/src/export/webdav/fs.go
+++ b/src/export/webdav/fs.go
@@ -10,7 +10,6 @@ import (
 	"time"
 
 	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
-	"git.kmsign.ru/royalcat/tstor/src/iio"
 	"golang.org/x/net/webdav"
 )
 
@@ -28,19 +27,19 @@ func (wd *WebDAV) OpenFile(ctx context.Context, name string, flag int, perm os.F
 	name = vfs.AbsPath(name)
 
 	// TODO handle flag and permissions
-	f, err := wd.lookupFile(name)
+	f, err := wd.lookupFile(ctx, name)
 	if err != nil {
 		return nil, err
 	}
 
-	wdf := newFile(path.Base(name), f, func() ([]fs.FileInfo, error) {
-		return wd.listDir(name)
+	wdf := newFile(ctx, path.Base(name), f, func() ([]fs.FileInfo, error) {
+		return wd.listDir(ctx, name)
 	})
 	return wdf, nil
 }
 
 func (wd *WebDAV) Stat(ctx context.Context, name string) (fs.FileInfo, error) {
-	return wd.fs.Stat(vfs.AbsPath(name))
+	return wd.fs.Stat(ctx, vfs.AbsPath(name))
 }
 
 func (wd *WebDAV) Mkdir(ctx context.Context, name string, perm fs.FileMode) error {
@@ -48,19 +47,19 @@ func (wd *WebDAV) Mkdir(ctx context.Context, name string, perm fs.FileMode) erro
 }
 
 func (wd *WebDAV) RemoveAll(ctx context.Context, name string) error {
-	return wd.fs.Unlink(name)
+	return wd.fs.Unlink(ctx, name)
 }
 
 func (wd *WebDAV) Rename(ctx context.Context, oldName, newName string) error {
 	return webdav.ErrNotImplemented
 }
 
-func (wd *WebDAV) lookupFile(name string) (vfs.File, error) {
-	return wd.fs.Open(path.Clean(name))
+func (wd *WebDAV) lookupFile(ctx context.Context, name string) (vfs.File, error) {
+	return wd.fs.Open(ctx, path.Clean(name))
 }
 
-func (wd *WebDAV) listDir(path string) ([]os.FileInfo, error) {
-	files, err := wd.fs.ReadDir(path)
+func (wd *WebDAV) listDir(ctx context.Context, path string) ([]os.FileInfo, error) {
+	files, err := wd.fs.ReadDir(ctx, path)
 	if err != nil {
 		return nil, err
 	}
@@ -80,9 +79,10 @@ func (wd *WebDAV) listDir(path string) ([]os.FileInfo, error) {
 var _ webdav.File = &webDAVFile{}
 
 type webDAVFile struct {
-	iio.Reader
+	ctx context.Context
 
 	fi os.FileInfo
+	f  vfs.File
 
 	mudp   sync.Mutex
 	dirPos int
@@ -93,11 +93,11 @@ type webDAVFile struct {
 	dirContent []os.FileInfo
 }
 
-func newFile(name string, f vfs.File, df func() ([]os.FileInfo, error)) *webDAVFile {
+func newFile(ctx context.Context, name string, f vfs.File, df func() ([]os.FileInfo, error)) *webDAVFile {
 	return &webDAVFile{
+		ctx:     ctx,
 		fi:      newFileInfo(name, f.Size(), f.IsDir()),
 		dirFunc: df,
-		Reader:  f,
 	}
 }
 
@@ -147,7 +147,7 @@ func (wdf *webDAVFile) Read(p []byte) (int, error) {
 	wdf.mup.Lock()
 	defer wdf.mup.Unlock()
 
-	n, err := wdf.Reader.ReadAt(p, wdf.pos)
+	n, err := wdf.f.ReadAt(wdf.ctx, p, wdf.pos)
 	wdf.pos += int64(n)
 
 	return n, err
@@ -173,6 +173,11 @@ func (wdf *webDAVFile) Write(p []byte) (n int, err error) {
 	return 0, webdav.ErrNotImplemented
 }
 
+// Close implements webdav.File.
+func (wdf *webDAVFile) Close() error {
+	return wdf.f.Close(wdf.ctx)
+}
+
 type webDAVFileInfo struct {
 	name  string
 	size  int64
diff --git a/src/host/service/service.go b/src/host/service/service.go
index 1d42a6a..c746119 100644
--- a/src/host/service/service.go
+++ b/src/host/service/service.go
@@ -9,8 +9,8 @@ import (
 	"path/filepath"
 	"slices"
 	"strings"
-	"time"
 
+	"git.kmsign.ru/royalcat/tstor/pkg/ctxio"
 	"git.kmsign.ru/royalcat/tstor/src/config"
 	"git.kmsign.ru/royalcat/tstor/src/host/controller"
 	"git.kmsign.ru/royalcat/tstor/src/host/datastorage"
@@ -46,13 +46,11 @@ type Service struct {
 
 	dirsAquire kv.Store[string, DirAquire]
 
-	log                     *slog.Logger
-	addTimeout, readTimeout int
+	log *slog.Logger
 }
 
 func NewService(sourceDir string, cfg config.TorrentClient, c *torrent.Client,
 	storage datastorage.DataStorage, excludedFiles *store.FilesMappings, infoBytes *store.InfoBytes,
-	addTimeout, readTimeout int,
 ) (*Service, error) {
 	dirsAcquire, err := kv.NewBadgerKV[string, DirAquire](path.Join(cfg.MetadataFolder, "dir-acquire"))
 	if err != nil {
@@ -70,8 +68,6 @@ func NewService(sourceDir string, cfg config.TorrentClient, c *torrent.Client,
 		torrentLoaded:   make(chan struct{}),
 		dirsAquire:      dirsAcquire,
 		// stats:       newStats(), // TODO persistent
-		addTimeout:  addTimeout,
-		readTimeout: readTimeout,
 	}
 
 	go func() {
@@ -94,14 +90,14 @@ func (s *Service) Close() error {
 }
 
 func (s *Service) AddTorrent(ctx context.Context, f vfs.File) (*torrent.Torrent, error) {
-	defer f.Close()
+	defer f.Close(ctx)
 
 	stat, err := f.Stat()
 	if err != nil {
 		return nil, fmt.Errorf("call stat failed: %w", err)
 	}
 
-	mi, err := metainfo.Load(f)
+	mi, err := metainfo.Load(ctxio.IoReader(ctx, f))
 	if err != nil {
 		return nil, fmt.Errorf("loading torrent metadata from file %s, error: %w", stat.Name(), err)
 	}
@@ -291,10 +287,8 @@ func isValidInfoHashBytes(d []byte) bool {
 	return err == nil
 }
 
-func (s *Service) NewTorrentFs(f vfs.File) (vfs.Filesystem, error) {
-	ctx, cancel := context.WithTimeout(context.TODO(), time.Second*time.Duration(s.addTimeout))
-	defer cancel()
-	defer f.Close()
+func (s *Service) NewTorrentFs(ctx context.Context, f vfs.File) (vfs.Filesystem, error) {
+	defer f.Close(ctx)
 
 	info, err := f.Stat()
 	if err != nil {
@@ -306,7 +300,7 @@ func (s *Service) NewTorrentFs(f vfs.File) (vfs.Filesystem, error) {
 		return nil, err
 	}
 
-	return vfs.NewTorrentFs(info.Name(), controller.NewTorrent(t, s.excludedFiles), s.readTimeout), nil
+	return vfs.NewTorrentFs(info.Name(), controller.NewTorrent(t, s.excludedFiles)), nil
 }
 
 func (s *Service) Stats() (*Stats, error) {
@@ -333,7 +327,7 @@ func (s *Service) loadTorrentFiles(ctx context.Context) error {
 
 		if strings.HasSuffix(path, ".torrent") {
 			file := vfs.NewLazyOsFile(path)
-			defer file.Close()
+			defer file.Close(ctx)
 
 			_, err = s.AddTorrent(ctx, file)
 			if err != nil {
diff --git a/src/host/vfs/archive.go b/src/host/vfs/archive.go
index b7bc60c..cf1e1a6 100644
--- a/src/host/vfs/archive.go
+++ b/src/host/vfs/archive.go
@@ -2,6 +2,7 @@ package vfs
 
 import (
 	"archive/zip"
+	"context"
 	"io"
 	"io/fs"
 	"os"
@@ -9,56 +10,57 @@ import (
 	"path/filepath"
 	"strings"
 
+	"git.kmsign.ru/royalcat/tstor/pkg/ctxio"
 	"git.kmsign.ru/royalcat/tstor/src/iio"
 	"github.com/bodgit/sevenzip"
 	"github.com/nwaples/rardecode/v2"
 )
 
 var ArchiveFactories = map[string]FsFactory{
-	".zip": func(f File) (Filesystem, error) {
+	".zip": func(ctx context.Context, f File) (Filesystem, error) {
 		stat, err := f.Stat()
 		if err != nil {
 			return nil, err
 		}
-		return NewArchive(stat.Name(), f, stat.Size(), ZipLoader), nil
+		return NewArchive(ctx, stat.Name(), f, stat.Size(), ZipLoader), nil
 	},
-	".rar": func(f File) (Filesystem, error) {
+	".rar": func(ctx context.Context, f File) (Filesystem, error) {
 		stat, err := f.Stat()
 		if err != nil {
 			return nil, err
 		}
-		return NewArchive(stat.Name(), f, stat.Size(), RarLoader), nil
+		return NewArchive(ctx, stat.Name(), f, stat.Size(), RarLoader), nil
 	},
-	".7z": func(f File) (Filesystem, error) {
+	".7z": func(ctx context.Context, f File) (Filesystem, error) {
 		stat, err := f.Stat()
 		if err != nil {
 			return nil, err
 		}
-		return NewArchive(stat.Name(), f, stat.Size(), SevenZipLoader), nil
+		return NewArchive(ctx, stat.Name(), f, stat.Size(), SevenZipLoader), nil
 	},
 }
 
-type archiveLoader func(r iio.Reader, size int64) (map[string]*archiveFile, error)
+type archiveLoader func(ctx context.Context, r ctxio.ReaderAt, size int64) (map[string]*archiveFile, error)
 
 var _ Filesystem = &ArchiveFS{}
 
 type ArchiveFS struct {
 	name string
 
-	r iio.Reader
+	r ctxio.ReaderAt
 
 	Size int64
 
 	files func() (map[string]File, error)
 }
 
-func NewArchive(name string, r iio.Reader, size int64, loader archiveLoader) *ArchiveFS {
+func NewArchive(ctx context.Context, name string, r ctxio.ReaderAt, size int64, loader archiveLoader) *ArchiveFS {
 	return &ArchiveFS{
 		name: name,
 		r:    r,
 		Size: size,
 		files: OnceValueWOErr(func() (map[string]File, error) {
-			zipFiles, err := loader(r, size)
+			zipFiles, err := loader(ctx, r, size)
 			if err != nil {
 				return nil, err
 			}
@@ -94,11 +96,11 @@ func NewArchive(name string, r iio.Reader, size int64, loader archiveLoader) *Ar
 }
 
 // Unlink implements Filesystem.
-func (a *ArchiveFS) Unlink(filename string) error {
+func (a *ArchiveFS) Unlink(ctx context.Context, filename string) error {
 	return ErrNotImplemented
 }
 
-func (a *ArchiveFS) Open(filename string) (File, error) {
+func (a *ArchiveFS) Open(ctx context.Context, filename string) (File, error) {
 	files, err := a.files()
 	if err != nil {
 		return nil, err
@@ -107,7 +109,7 @@ func (a *ArchiveFS) Open(filename string) (File, error) {
 	return getFile(files, filename)
 }
 
-func (fs *ArchiveFS) ReadDir(path string) ([]fs.DirEntry, error) {
+func (fs *ArchiveFS) ReadDir(ctx context.Context, path string) ([]fs.DirEntry, error) {
 	files, err := fs.files()
 	if err != nil {
 		return nil, err
@@ -117,7 +119,7 @@ func (fs *ArchiveFS) ReadDir(path string) ([]fs.DirEntry, error) {
 }
 
 // Stat implements Filesystem.
-func (afs *ArchiveFS) Stat(filename string) (fs.FileInfo, error) {
+func (afs *ArchiveFS) Stat(ctx context.Context, filename string) (fs.FileInfo, error) {
 	files, err := afs.files()
 	if err != nil {
 		return nil, err
@@ -204,7 +206,7 @@ func (d *archiveFile) IsDir() bool {
 	return false
 }
 
-func (d *archiveFile) Close() (err error) {
+func (d *archiveFile) Close(ctx context.Context) (err error) {
 	if d.reader != nil {
 		err = d.reader.Close()
 		d.reader = nil
@@ -213,7 +215,7 @@ func (d *archiveFile) Close() (err error) {
 	return
 }
 
-func (d *archiveFile) Read(p []byte) (n int, err error) {
+func (d *archiveFile) Read(ctx context.Context, p []byte) (n int, err error) {
 	if err := d.load(); err != nil {
 		return 0, err
 	}
@@ -221,7 +223,7 @@ func (d *archiveFile) Read(p []byte) (n int, err error) {
 	return d.reader.Read(p)
 }
 
-func (d *archiveFile) ReadAt(p []byte, off int64) (n int, err error) {
+func (d *archiveFile) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) {
 	if err := d.load(); err != nil {
 		return 0, err
 	}
@@ -231,7 +233,9 @@ func (d *archiveFile) ReadAt(p []byte, off int64) (n int, err error) {
 
 var _ archiveLoader = ZipLoader
 
-func ZipLoader(reader iio.Reader, size int64) (map[string]*archiveFile, error) {
+func ZipLoader(ctx context.Context, ctxreader ctxio.ReaderAt, size int64) (map[string]*archiveFile, error) {
+	reader := ctxio.IoReaderAt(ctx, ctxreader)
+
 	zr, err := zip.NewReader(reader, size)
 	if err != nil {
 		return nil, err
@@ -261,7 +265,9 @@ func ZipLoader(reader iio.Reader, size int64) (map[string]*archiveFile, error) {
 
 var _ archiveLoader = SevenZipLoader
 
-func SevenZipLoader(reader iio.Reader, size int64) (map[string]*archiveFile, error) {
+func SevenZipLoader(ctx context.Context, ctxreader ctxio.ReaderAt, size int64) (map[string]*archiveFile, error) {
+	reader := ctxio.IoReaderAt(ctx, ctxreader)
+
 	r, err := sevenzip.NewReader(reader, size)
 	if err != nil {
 		return nil, err
@@ -294,8 +300,10 @@ func SevenZipLoader(reader iio.Reader, size int64) (map[string]*archiveFile, err
 
 var _ archiveLoader = RarLoader
 
-func RarLoader(reader iio.Reader, size int64) (map[string]*archiveFile, error) {
-	r, err := rardecode.NewReader(iio.NewSeekerWrapper(reader, size))
+func RarLoader(ctx context.Context, ctxreader ctxio.ReaderAt, size int64) (map[string]*archiveFile, error) {
+	reader := ctxio.IoReadSeekerWrapper(ctx, ctxreader, size)
+
+	r, err := rardecode.NewReader(reader)
 	if err != nil {
 		return nil, err
 	}
diff --git a/src/host/vfs/archive_test.go b/src/host/vfs/archive_test.go
index c174438..c6ae444 100644
--- a/src/host/vfs/archive_test.go
+++ b/src/host/vfs/archive_test.go
@@ -3,10 +3,11 @@ package vfs
 import (
 	"archive/zip"
 	"bytes"
+	"context"
 	"io"
 	"testing"
 
-	"git.kmsign.ru/royalcat/tstor/src/iio"
+	"git.kmsign.ru/royalcat/tstor/pkg/ctxio"
 	"github.com/stretchr/testify/require"
 )
 
@@ -18,10 +19,12 @@ func TestZipFilesystem(t *testing.T) {
 
 	zReader, size := createTestZip(require)
 
-	// TODO add single dir collapse test
-	zfs := NewArchive("test", zReader, size, ZipLoader)
+	ctx := context.Background()
 
-	files, err := zfs.ReadDir("/path/to/test/file")
+	// TODO add single dir collapse test
+	zfs := NewArchive(ctx, "test", zReader, size, ZipLoader)
+
+	files, err := zfs.ReadDir(ctx, "/path/to/test/file")
 	require.NoError(err)
 
 	require.Len(files, 1)
@@ -30,16 +33,16 @@ func TestZipFilesystem(t *testing.T) {
 	require.NotNil(e)
 
 	out := make([]byte, 11)
-	f, err := zfs.Open("/path/to/test/file/1.txt")
+	f, err := zfs.Open(ctx, "/path/to/test/file/1.txt")
 	require.NoError(err)
-	n, err := f.Read(out)
+	n, err := f.Read(ctx, out)
 	require.Equal(io.EOF, err)
 	require.Equal(11, n)
 	require.Equal(fileContent, out)
 
 }
 
-func createTestZip(require *require.Assertions) (iio.Reader, int64) {
+func createTestZip(require *require.Assertions) (ctxio.ReaderAt, int64) {
 	buf := bytes.NewBuffer([]byte{})
 
 	zWriter := zip.NewWriter(buf)
@@ -56,15 +59,16 @@ func createTestZip(require *require.Assertions) (iio.Reader, int64) {
 }
 
 type closeableByteReader struct {
-	*bytes.Reader
+	data *bytes.Reader
 }
 
 func newCBR(b []byte) *closeableByteReader {
 	return &closeableByteReader{
-		Reader: bytes.NewReader(b),
+		data: bytes.NewReader(b),
 	}
 }
 
-func (*closeableByteReader) Close() error {
-	return nil
+// ReadAt implements ctxio.ReaderAt.
+func (c *closeableByteReader) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) {
+	return c.data.ReadAt(p, off)
 }
diff --git a/src/host/vfs/dir.go b/src/host/vfs/dir.go
index fc0c1a0..6d77249 100644
--- a/src/host/vfs/dir.go
+++ b/src/host/vfs/dir.go
@@ -1,6 +1,7 @@
 package vfs
 
 import (
+	"context"
 	"io/fs"
 	"path"
 )
@@ -30,14 +31,14 @@ func (d *dir) IsDir() bool {
 	return true
 }
 
-func (d *dir) Close() error {
+func (d *dir) Close(ctx context.Context) error {
 	return nil
 }
 
-func (d *dir) Read(p []byte) (n int, err error) {
+func (d *dir) Read(ctx context.Context, p []byte) (n int, err error) {
 	return 0, nil
 }
 
-func (d *dir) ReadAt(p []byte, off int64) (n int, err error) {
+func (d *dir) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) {
 	return 0, nil
 }
diff --git a/src/host/vfs/fs.go b/src/host/vfs/fs.go
index 08282d3..018967f 100644
--- a/src/host/vfs/fs.go
+++ b/src/host/vfs/fs.go
@@ -1,12 +1,13 @@
 package vfs
 
 import (
+	"context"
 	"errors"
 	"io/fs"
 	"path"
 	"time"
 
-	"git.kmsign.ru/royalcat/tstor/src/iio"
+	"git.kmsign.ru/royalcat/tstor/pkg/ctxio"
 )
 
 type File interface {
@@ -14,7 +15,9 @@ type File interface {
 	Size() int64
 	Stat() (fs.FileInfo, error)
 
-	iio.Reader
+	ctxio.Reader
+	ctxio.ReaderAt
+	ctxio.Closer
 }
 
 var ErrNotImplemented = errors.New("not implemented")
@@ -23,14 +26,14 @@ type Filesystem interface {
 	// Open opens the named file for reading. If successful, methods on the
 	// returned file can be used for reading; the associated file descriptor has
 	// mode O_RDONLY.
-	Open(filename string) (File, error)
+	Open(ctx context.Context, filename string) (File, error)
 
 	// ReadDir reads the directory named by dirname and returns a list of
 	// directory entries.
-	ReadDir(path string) ([]fs.DirEntry, error)
+	ReadDir(ctx context.Context, path string) ([]fs.DirEntry, error)
 
-	Stat(filename string) (fs.FileInfo, error)
-	Unlink(filename string) error
+	Stat(ctx context.Context, filename string) (fs.FileInfo, error)
+	Unlink(ctx context.Context, filename string) error
 
 	fs.DirEntry
 }
diff --git a/src/host/vfs/log.go b/src/host/vfs/log.go
index de85be3..7560f26 100644
--- a/src/host/vfs/log.go
+++ b/src/host/vfs/log.go
@@ -1,8 +1,10 @@
 package vfs
 
 import (
+	"context"
 	"io/fs"
 	"log/slog"
+	"reflect"
 )
 
 type LogFS struct {
@@ -40,8 +42,8 @@ func (fs *LogFS) Type() fs.FileMode {
 }
 
 // Open implements Filesystem.
-func (fs *LogFS) Open(filename string) (File, error) {
-	file, err := fs.fs.Open(filename)
+func (fs *LogFS) Open(ctx context.Context, filename string) (File, error) {
+	file, err := fs.fs.Open(ctx, filename)
 	if err != nil {
 		fs.log.With("filename", filename).Error("Failed to open file")
 	}
@@ -50,17 +52,17 @@ func (fs *LogFS) Open(filename string) (File, error) {
 }
 
 // ReadDir implements Filesystem.
-func (fs *LogFS) ReadDir(path string) ([]fs.DirEntry, error) {
-	file, err := fs.fs.ReadDir(path)
+func (fs *LogFS) ReadDir(ctx context.Context, path string) ([]fs.DirEntry, error) {
+	file, err := fs.fs.ReadDir(ctx, path)
 	if err != nil {
-		fs.log.Error("Failed to read dir", "path", path, "error", err)
+		fs.log.ErrorContext(ctx, "Failed to read dir", "path", path, "error", err.Error(), "fs-type", reflect.TypeOf(fs.fs).Name())
 	}
 	return file, err
 }
 
 // Stat implements Filesystem.
-func (fs *LogFS) Stat(filename string) (fs.FileInfo, error) {
-	file, err := fs.fs.Stat(filename)
+func (fs *LogFS) Stat(ctx context.Context, filename string) (fs.FileInfo, error) {
+	file, err := fs.fs.Stat(ctx, filename)
 	if err != nil {
 		fs.log.Error("Failed to stat", "filename", filename, "error", err)
 	}
@@ -68,8 +70,8 @@ func (fs *LogFS) Stat(filename string) (fs.FileInfo, error) {
 }
 
 // Unlink implements Filesystem.
-func (fs *LogFS) Unlink(filename string) error {
-	err := fs.fs.Unlink(filename)
+func (fs *LogFS) Unlink(ctx context.Context, filename string) error {
+	err := fs.fs.Unlink(ctx, filename)
 	if err != nil {
 		fs.log.Error("Failed to stat", "filename", filename, "error", err)
 	}
@@ -91,8 +93,8 @@ func WrapLogFile(f File, filename string, log *slog.Logger) *LogFile {
 }
 
 // Close implements File.
-func (f *LogFile) Close() error {
-	err := f.f.Close()
+func (f *LogFile) Close(ctx context.Context) error {
+	err := f.f.Close(ctx)
 	if err != nil {
 		f.log.Error("Failed to close", "error", err)
 	}
@@ -105,8 +107,8 @@ func (f *LogFile) IsDir() bool {
 }
 
 // Read implements File.
-func (f *LogFile) Read(p []byte) (n int, err error) {
-	n, err = f.f.Read(p)
+func (f *LogFile) Read(ctx context.Context, p []byte) (n int, err error) {
+	n, err = f.f.Read(ctx, p)
 	if err != nil {
 		f.log.Error("Failed to read", "error", err)
 	}
@@ -114,8 +116,8 @@ func (f *LogFile) Read(p []byte) (n int, err error) {
 }
 
 // ReadAt implements File.
-func (f *LogFile) ReadAt(p []byte, off int64) (n int, err error) {
-	n, err = f.f.ReadAt(p, off)
+func (f *LogFile) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) {
+	n, err = f.f.ReadAt(ctx, p, off)
 	if err != nil {
 		f.log.Error("Failed to read", "offset", off, "error", err)
 	}
diff --git a/src/host/vfs/memory.go b/src/host/vfs/memory.go
index cd0680a..79d2f0e 100644
--- a/src/host/vfs/memory.go
+++ b/src/host/vfs/memory.go
@@ -2,6 +2,7 @@ package vfs
 
 import (
 	"bytes"
+	"context"
 	"io/fs"
 	"path"
 )
@@ -33,11 +34,6 @@ func (mfs *MemoryFs) Type() fs.FileMode {
 	return fs.ModeDir
 }
 
-// Unlink implements Filesystem.
-func (fs *MemoryFs) Unlink(filename string) error {
-	return ErrNotImplemented
-}
-
 func NewMemoryFS(name string, files map[string]*MemoryFile) *MemoryFs {
 	return &MemoryFs{
 		name:  name,
@@ -45,16 +41,16 @@ func NewMemoryFS(name string, files map[string]*MemoryFile) *MemoryFs {
 	}
 }
 
-func (m *MemoryFs) Open(filename string) (File, error) {
+func (m *MemoryFs) Open(ctx context.Context, filename string) (File, error) {
 	return getFile(m.files, filename)
 }
 
-func (fs *MemoryFs) ReadDir(path string) ([]fs.DirEntry, error) {
+func (fs *MemoryFs) ReadDir(ctx context.Context, path string) ([]fs.DirEntry, error) {
 	return listDirFromFiles(fs.files, path)
 }
 
 // Stat implements Filesystem.
-func (mfs *MemoryFs) Stat(filename string) (fs.FileInfo, error) {
+func (mfs *MemoryFs) Stat(ctx context.Context, filename string) (fs.FileInfo, error) {
 	file, ok := mfs.files[filename]
 	if !ok {
 		return nil, ErrNotExist
@@ -62,32 +58,47 @@ func (mfs *MemoryFs) Stat(filename string) (fs.FileInfo, error) {
 	return newFileInfo(path.Base(filename), file.Size()), nil
 }
 
-var _ File = &MemoryFile{}
+// Unlink implements Filesystem.
+func (fs *MemoryFs) Unlink(ctx context.Context, filename string) error {
+	return ErrNotImplemented
+}
+
+var _ File = (*MemoryFile)(nil)
 
 type MemoryFile struct {
 	name string
-	*bytes.Reader
+	data *bytes.Reader
 }
 
 func NewMemoryFile(name string, data []byte) *MemoryFile {
 	return &MemoryFile{
-		name:   name,
-		Reader: bytes.NewReader(data),
+		name: name,
+		data: bytes.NewReader(data),
 	}
 }
 
 func (d *MemoryFile) Stat() (fs.FileInfo, error) {
-	return newFileInfo(d.name, int64(d.Reader.Len())), nil
+	return newFileInfo(d.name, int64(d.data.Len())), nil
 }
 
 func (d *MemoryFile) Size() int64 {
-	return int64(d.Reader.Len())
+	return int64(d.data.Len())
 }
 
 func (d *MemoryFile) IsDir() bool {
 	return false
 }
 
-func (d *MemoryFile) Close() (err error) {
+func (d *MemoryFile) Close(ctx context.Context) (err error) {
 	return
 }
+
+// Read implements File.
+func (d *MemoryFile) Read(ctx context.Context, p []byte) (n int, err error) {
+	return d.data.Read(p)
+}
+
+// ReadAt implements File.
+func (d *MemoryFile) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) {
+	return d.data.ReadAt(p, off)
+}
diff --git a/src/host/vfs/memory_test.go b/src/host/vfs/memory_test.go
index a174921..6f6788f 100644
--- a/src/host/vfs/memory_test.go
+++ b/src/host/vfs/memory_test.go
@@ -1,6 +1,7 @@
 package vfs
 
 import (
+	"context"
 	"testing"
 
 	"github.com/stretchr/testify/require"
@@ -11,6 +12,7 @@ func TestMemory(t *testing.T) {
 
 	require := require.New(t)
 	testData := "Hello"
+	ctx := context.Background()
 
 	c := NewMemoryFS("/", map[string]*MemoryFile{
 		"/dir/here": NewMemoryFile("here", []byte(testData)),
@@ -23,23 +25,23 @@ func TestMemory(t *testing.T) {
 	// c, err := NewContainerFs(fss)
 	// require.NoError(err)
 
-	f, err := c.Open("/dir/here")
+	f, err := c.Open(ctx, "/dir/here")
 	require.NoError(err)
 	require.NotNil(f)
 	require.Equal(int64(5), f.Size())
-	require.NoError(f.Close())
+	require.NoError(f.Close(ctx))
 
 	data := make([]byte, 5)
-	n, err := f.Read(data)
+	n, err := f.Read(ctx, data)
 	require.NoError(err)
 	require.Equal(5, n)
 	require.Equal(string(data), testData)
 
-	files, err := c.ReadDir("/")
+	files, err := c.ReadDir(ctx, "/")
 	require.NoError(err)
 	require.Len(files, 1)
 
-	files, err = c.ReadDir("/dir")
+	files, err = c.ReadDir(ctx, "/dir")
 	require.NoError(err)
 	require.Len(files, 1)
 
diff --git a/src/host/vfs/os.go b/src/host/vfs/os.go
index 9b0277a..18daf11 100644
--- a/src/host/vfs/os.go
+++ b/src/host/vfs/os.go
@@ -1,6 +1,7 @@
 package vfs
 
 import (
+	"context"
 	"io/fs"
 	"os"
 	"path"
@@ -12,7 +13,7 @@ type OsFS struct {
 }
 
 // Stat implements Filesystem.
-func (fs *OsFS) Stat(filename string) (fs.FileInfo, error) {
+func (fs *OsFS) Stat(ctx context.Context, filename string) (fs.FileInfo, error) {
 	if path.Clean(filename) == Separator {
 		return newDirInfo(Separator), nil
 	}
@@ -21,12 +22,12 @@ func (fs *OsFS) Stat(filename string) (fs.FileInfo, error) {
 }
 
 // Unlink implements Filesystem.
-func (fs *OsFS) Unlink(filename string) error {
+func (fs *OsFS) Unlink(ctx context.Context, filename string) error {
 	return os.RemoveAll(path.Join(fs.hostDir, filename))
 }
 
 // Open implements Filesystem.
-func (fs *OsFS) Open(filename string) (File, error) {
+func (fs *OsFS) Open(ctx context.Context, filename string) (File, error) {
 	if path.Clean(filename) == Separator {
 		return NewDir(filename), nil
 	}
@@ -35,7 +36,7 @@ func (fs *OsFS) Open(filename string) (File, error) {
 }
 
 // ReadDir implements Filesystem.
-func (o *OsFS) ReadDir(dir string) ([]fs.DirEntry, error) {
+func (o *OsFS) ReadDir(ctx context.Context, dir string) ([]fs.DirEntry, error) {
 	return os.ReadDir(path.Join(o.hostDir, dir))
 }
 
@@ -83,17 +84,17 @@ func (f *OsFile) Info() (fs.FileInfo, error) {
 }
 
 // Close implements File.
-func (f *OsFile) Close() error {
+func (f *OsFile) Close(ctx context.Context) error {
 	return f.f.Close()
 }
 
 // Read implements File.
-func (f *OsFile) Read(p []byte) (n int, err error) {
+func (f *OsFile) Read(ctx context.Context, p []byte) (n int, err error) {
 	return f.f.Read(p)
 }
 
 // ReadAt implements File.
-func (f *OsFile) ReadAt(p []byte, off int64) (n int, err error) {
+func (f *OsFile) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) {
 	return f.f.ReadAt(p, off)
 }
 
@@ -151,7 +152,7 @@ func (f *LazyOsFile) open() error {
 }
 
 // Close implements File.
-func (f *LazyOsFile) Close() error {
+func (f *LazyOsFile) Close(ctx context.Context) error {
 	if f.file == nil {
 		return nil
 	}
@@ -159,7 +160,7 @@ func (f *LazyOsFile) Close() error {
 }
 
 // Read implements File.
-func (f *LazyOsFile) Read(p []byte) (n int, err error) {
+func (f *LazyOsFile) Read(ctx context.Context, p []byte) (n int, err error) {
 	err = f.open()
 	if err != nil {
 		return 0, err
@@ -168,7 +169,7 @@ func (f *LazyOsFile) Read(p []byte) (n int, err error) {
 }
 
 // ReadAt implements File.
-func (f *LazyOsFile) ReadAt(p []byte, off int64) (n int, err error) {
+func (f *LazyOsFile) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) {
 	err = f.open()
 	if err != nil {
 		return 0, err
diff --git a/src/host/vfs/resolver.go b/src/host/vfs/resolver.go
index b465b7c..d434099 100644
--- a/src/host/vfs/resolver.go
+++ b/src/host/vfs/resolver.go
@@ -1,6 +1,7 @@
 package vfs
 
 import (
+	"context"
 	"fmt"
 	"io/fs"
 	"path"
@@ -22,41 +23,41 @@ func NewResolveFS(rootFs Filesystem, factories map[string]FsFactory) *ResolverFS
 }
 
 // Open implements Filesystem.
-func (r *ResolverFS) Open(filename string) (File, error) {
-	fsPath, nestedFs, nestedFsPath, err := r.resolver.resolvePath(filename, r.rootFS.Open)
+func (r *ResolverFS) Open(ctx context.Context, filename string) (File, error) {
+	fsPath, nestedFs, nestedFsPath, err := r.resolver.resolvePath(ctx, filename, r.rootFS.Open)
 	if err != nil {
 		return nil, err
 	}
 	if nestedFs != nil {
-		return nestedFs.Open(nestedFsPath)
+		return nestedFs.Open(ctx, nestedFsPath)
 	}
 
-	return r.rootFS.Open(fsPath)
+	return r.rootFS.Open(ctx, fsPath)
 }
 
 // ReadDir implements Filesystem.
-func (r *ResolverFS) ReadDir(dir string) ([]fs.DirEntry, error) {
-	fsPath, nestedFs, nestedFsPath, err := r.resolver.resolvePath(dir, r.rootFS.Open)
+func (r *ResolverFS) ReadDir(ctx context.Context, dir string) ([]fs.DirEntry, error) {
+	fsPath, nestedFs, nestedFsPath, err := r.resolver.resolvePath(ctx, dir, r.rootFS.Open)
 	if err != nil {
 		return nil, err
 	}
 	if nestedFs != nil {
-		return nestedFs.ReadDir(nestedFsPath)
+		return nestedFs.ReadDir(ctx, nestedFsPath)
 	}
 
-	entries, err := r.rootFS.ReadDir(fsPath)
+	entries, err := r.rootFS.ReadDir(ctx, fsPath)
 	if err != nil {
 		return nil, err
 	}
 	out := make([]fs.DirEntry, 0, len(entries))
 	for _, e := range entries {
 		if r.resolver.isNestedFs(e.Name()) {
-			filepath := path.Join(dir, e.Name())
-			file, err := r.Open(filepath)
+			filepath := path.Join("/", dir, e.Name())
+			file, err := r.Open(ctx, filepath)
 			if err != nil {
 				return nil, err
 			}
-			nestedfs, err := r.resolver.nestedFs(filepath, file)
+			nestedfs, err := r.resolver.nestedFs(ctx, filepath, file)
 			if err != nil {
 				return nil, err
 			}
@@ -70,29 +71,29 @@ func (r *ResolverFS) ReadDir(dir string) ([]fs.DirEntry, error) {
 }
 
 // Stat implements Filesystem.
-func (r *ResolverFS) Stat(filename string) (fs.FileInfo, error) {
-	fsPath, nestedFs, nestedFsPath, err := r.resolver.resolvePath(filename, r.rootFS.Open)
+func (r *ResolverFS) Stat(ctx context.Context, filename string) (fs.FileInfo, error) {
+	fsPath, nestedFs, nestedFsPath, err := r.resolver.resolvePath(ctx, filename, r.rootFS.Open)
 	if err != nil {
 		return nil, err
 	}
 	if nestedFs != nil {
-		return nestedFs.Stat(nestedFsPath)
+		return nestedFs.Stat(ctx, nestedFsPath)
 	}
 
-	return r.rootFS.Stat(fsPath)
+	return r.rootFS.Stat(ctx, fsPath)
 }
 
 // Unlink implements Filesystem.
-func (r *ResolverFS) Unlink(filename string) error {
-	fsPath, nestedFs, nestedFsPath, err := r.resolver.resolvePath(filename, r.rootFS.Open)
+func (r *ResolverFS) Unlink(ctx context.Context, filename string) error {
+	fsPath, nestedFs, nestedFsPath, err := r.resolver.resolvePath(ctx, filename, r.rootFS.Open)
 	if err != nil {
 		return err
 	}
 	if nestedFs != nil {
-		return nestedFs.Unlink(nestedFsPath)
+		return nestedFs.Unlink(ctx, nestedFsPath)
 	}
 
-	return r.rootFS.Unlink(fsPath)
+	return r.rootFS.Unlink(ctx, fsPath)
 }
 
 // Info implements Filesystem.
@@ -117,7 +118,7 @@ func (r *ResolverFS) Type() fs.FileMode {
 
 var _ Filesystem = &ResolverFS{}
 
-type FsFactory func(f File) (Filesystem, error)
+type FsFactory func(ctx context.Context, f File) (Filesystem, error)
 
 const Separator = "/"
 
@@ -135,7 +136,7 @@ type resolver struct {
 	// TODO: add fsmap clean
 }
 
-type openFile func(path string) (File, error)
+type openFile func(ctx context.Context, path string) (File, error)
 
 func (r *resolver) isNestedFs(f string) bool {
 	for ext := range r.factories {
@@ -146,7 +147,7 @@ func (r *resolver) isNestedFs(f string) bool {
 	return false
 }
 
-func (r *resolver) nestedFs(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
@@ -156,7 +157,7 @@ func (r *resolver) nestedFs(fsPath string, file File) (Filesystem, error) {
 			return nestedFs, nil
 		}
 
-		nestedFs, err := nestFactory(file)
+		nestedFs, err := nestFactory(ctx, file)
 		if err != nil {
 			return nil, fmt.Errorf("error creating filesystem from file: %s with error: %w", fsPath, err)
 		}
@@ -169,7 +170,7 @@ func (r *resolver) nestedFs(fsPath string, file File) (Filesystem, error) {
 }
 
 // open requeue raw open, without resolver call
-func (r *resolver) resolvePath(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) {
 	name = path.Clean(name)
 	name = strings.TrimPrefix(name, Separator)
 	parts := strings.Split(name, Separator)
@@ -206,11 +207,11 @@ PARTS_LOOP:
 	if nestedFs, ok := r.fsmap[fsPath]; ok {
 		return fsPath, nestedFs, nestedFsPath, nil
 	} else {
-		fsFile, err := rawOpen(fsPath)
+		fsFile, err := rawOpen(ctx, fsPath)
 		if err != nil {
 			return "", nil, "", fmt.Errorf("error opening filesystem file: %s with error: %w", fsPath, err)
 		}
-		nestedFs, err := nestFactory(fsFile)
+		nestedFs, err := nestFactory(ctx, fsFile)
 		if err != nil {
 			return "", nil, "", fmt.Errorf("error creating filesystem from file: %s with error: %w", fsPath, err)
 		}
diff --git a/src/host/vfs/resolver_test.go b/src/host/vfs/resolver_test.go
index c0cef9a..ab801f9 100644
--- a/src/host/vfs/resolver_test.go
+++ b/src/host/vfs/resolver_test.go
@@ -1,6 +1,7 @@
 package vfs
 
 import (
+	"context"
 	"io/fs"
 	"os"
 	"path"
@@ -26,15 +27,15 @@ func (d *Dummy) IsDir() bool {
 	return false
 }
 
-func (d *Dummy) Close() error {
+func (d *Dummy) Close(ctx context.Context) error {
 	return nil
 }
 
-func (d *Dummy) Read(p []byte) (n int, err error) {
+func (d *Dummy) Read(ctx context.Context, p []byte) (n int, err error) {
 	return 0, nil
 }
 
-func (d *Dummy) ReadAt(p []byte, off int64) (n int, err error) {
+func (d *Dummy) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) {
 	return 0, nil
 }
 
@@ -45,19 +46,19 @@ type DummyFs struct {
 }
 
 // Stat implements Filesystem.
-func (*DummyFs) Stat(filename string) (fs.FileInfo, error) {
+func (*DummyFs) Stat(ctx context.Context, filename string) (fs.FileInfo, error) {
 	return newFileInfo(path.Base(filename), 0), nil // TODO
 }
 
-func (d *DummyFs) Open(filename string) (File, error) {
+func (d *DummyFs) Open(ctx context.Context, filename string) (File, error) {
 	return &Dummy{}, nil
 }
 
-func (d *DummyFs) Unlink(filename string) error {
+func (d *DummyFs) Unlink(ctx context.Context, filename string) error {
 	return ErrNotImplemented
 }
 
-func (d *DummyFs) ReadDir(path string) ([]fs.DirEntry, error) {
+func (d *DummyFs) ReadDir(ctx context.Context, path string) ([]fs.DirEntry, error) {
 	if path == "/dir/here" {
 		return []fs.DirEntry{
 			newFileInfo("file1.txt", 0),
@@ -93,11 +94,13 @@ var _ Filesystem = &DummyFs{}
 func TestResolver(t *testing.T) {
 	t.Parallel()
 	resolver := newResolver(ArchiveFactories)
+	ctx := context.Background()
+
 	t.Run("nested fs", func(t *testing.T) {
 		t.Parallel()
 		require := require.New(t)
 
-		fsPath, nestedFs, nestedFsPath, err := resolver.resolvePath("/f1.rar/f2.rar", func(path string) (File, error) {
+		fsPath, nestedFs, nestedFsPath, err := resolver.resolvePath(ctx, "/f1.rar/f2.rar", func(_ context.Context, path string) (File, error) {
 			require.Equal("/f1.rar", path)
 			return &Dummy{}, nil
 		})
@@ -110,7 +113,7 @@ func TestResolver(t *testing.T) {
 		t.Parallel()
 		require := require.New(t)
 
-		fsPath, nestedFs, nestedFsPath, err := resolver.resolvePath("/", func(path string) (File, error) {
+		fsPath, nestedFs, nestedFsPath, err := resolver.resolvePath(ctx, "/", func(_ context.Context, path string) (File, error) {
 			require.Equal("/", path)
 			return &Dummy{}, nil
 		})
@@ -124,7 +127,7 @@ func TestResolver(t *testing.T) {
 		t.Parallel()
 		require := require.New(t)
 
-		fsPath, nestedFs, nestedFsPath, err := resolver.resolvePath("//.//", func(path string) (File, error) {
+		fsPath, nestedFs, nestedFsPath, err := resolver.resolvePath(ctx, "//.//", func(_ context.Context, path string) (File, error) {
 			require.Equal("/", path)
 			return &Dummy{}, nil
 		})
@@ -137,7 +140,7 @@ func TestResolver(t *testing.T) {
 		t.Parallel()
 		require := require.New(t)
 
-		fsPath, nestedFs, nestedFsPath, err := resolver.resolvePath("//.//f1.rar", func(path string) (File, error) {
+		fsPath, nestedFs, nestedFsPath, err := resolver.resolvePath(ctx, "//.//f1.rar", func(_ context.Context, path string) (File, error) {
 			require.Equal("/f1.rar", path)
 			return &Dummy{}, nil
 		})
@@ -150,7 +153,7 @@ func TestResolver(t *testing.T) {
 		t.Parallel()
 		require := require.New(t)
 
-		fsPath, nestedFs, nestedFsPath, err := resolver.resolvePath("//test1/f1.rar", func(path string) (File, error) {
+		fsPath, nestedFs, nestedFsPath, err := resolver.resolvePath(ctx, "//test1/f1.rar", func(_ context.Context, path string) (File, error) {
 			require.Equal("/test1/f1.rar", path)
 			return &Dummy{}, nil
 		})
@@ -164,21 +167,23 @@ func TestResolver(t *testing.T) {
 func TestArchiveFactories(t *testing.T) {
 	t.Parallel()
 
+	ctx := context.Background()
+
 	require := require.New(t)
 
 	require.Contains(ArchiveFactories, ".zip")
 	require.Contains(ArchiveFactories, ".rar")
 	require.Contains(ArchiveFactories, ".7z")
 
-	fs, err := ArchiveFactories[".zip"](&Dummy{})
+	fs, err := ArchiveFactories[".zip"](ctx, &Dummy{})
 	require.NoError(err)
 	require.NotNil(fs)
 
-	fs, err = ArchiveFactories[".rar"](&Dummy{})
+	fs, err = ArchiveFactories[".rar"](ctx, &Dummy{})
 	require.NoError(err)
 	require.NotNil(fs)
 
-	fs, err = ArchiveFactories[".7z"](&Dummy{})
+	fs, err = ArchiveFactories[".7z"](ctx, &Dummy{})
 	require.NoError(err)
 	require.NotNil(fs)
 }
diff --git a/src/host/vfs/torrent.go b/src/host/vfs/torrent.go
index 4b51e9d..7f4545b 100644
--- a/src/host/vfs/torrent.go
+++ b/src/host/vfs/torrent.go
@@ -8,12 +8,9 @@ import (
 	"slices"
 	"strings"
 	"sync"
-	"time"
 
+	"git.kmsign.ru/royalcat/tstor/pkg/ctxio"
 	"git.kmsign.ru/royalcat/tstor/src/host/controller"
-	"git.kmsign.ru/royalcat/tstor/src/iio"
-	"github.com/RoaringBitmap/roaring"
-	"github.com/anacrolix/missinggo/v2"
 	"github.com/anacrolix/torrent"
 	"golang.org/x/exp/maps"
 )
@@ -26,19 +23,16 @@ type TorrentFs struct {
 	mu      sync.Mutex
 	Torrent *controller.Torrent
 
-	readTimeout int
-
 	filesCache map[string]File
 
 	resolver *resolver
 }
 
-func NewTorrentFs(name string, c *controller.Torrent, readTimeout int) *TorrentFs {
+func NewTorrentFs(name string, c *controller.Torrent) *TorrentFs {
 	return &TorrentFs{
-		name:        name,
-		Torrent:     c,
-		readTimeout: readTimeout,
-		resolver:    newResolver(ArchiveFactories),
+		name:     name,
+		Torrent:  c,
+		resolver: newResolver(ArchiveFactories),
 	}
 }
 
@@ -64,7 +58,7 @@ func (tfs *TorrentFs) Type() fs.FileMode {
 	return fs.ModeDir
 }
 
-func (fs *TorrentFs) files() (map[string]File, error) {
+func (fs *TorrentFs) files(ctx context.Context) (map[string]File, error) {
 	fs.mu.Lock()
 	defer fs.mu.Unlock()
 
@@ -81,26 +75,25 @@ func (fs *TorrentFs) files() (map[string]File, error) {
 	for _, file := range files {
 		file.Download()
 		p := AbsPath(file.Path())
-
-		fs.filesCache[p] = &torrentFile{
-			name:    path.Base(p),
-			timeout: fs.readTimeout,
-			file:    file,
+		tf, err := openTorrentFile(ctx, path.Base(p), file)
+		if err != nil {
+			return nil, err
 		}
+		fs.filesCache[p] = tf
 	}
 
 	// TODO optional
 	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(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 = listFilesRecursive(nestedFs, "/")
+			fs.filesCache, err = listFilesRecursive(ctx, nestedFs, "/")
 			if err != nil {
 				return nil, err
 			}
@@ -130,40 +123,40 @@ 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 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 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 listFilesRecursive(vfs Filesystem, start string) (map[string]File, error) {
+func listFilesRecursive(ctx context.Context, vfs Filesystem, start string) (map[string]File, error) {
 	out := make(map[string]File, 0)
-	entries, err := vfs.ReadDir(start)
+	entries, err := vfs.ReadDir(ctx, start)
 	if err != nil {
 		return nil, err
 	}
 	for _, entry := range entries {
 		filename := path.Join(start, entry.Name())
 		if entry.IsDir() {
-			rec, err := listFilesRecursive(vfs, filename)
+			rec, err := listFilesRecursive(ctx, vfs, filename)
 			if err != nil {
 				return nil, err
 			}
 			maps.Copy(out, rec)
 		} else {
-			file, err := vfs.Open(filename)
+			file, err := vfs.Open(ctx, filename)
 			if err != nil {
 				return nil, err
 			}
@@ -174,8 +167,8 @@ func listFilesRecursive(vfs Filesystem, start string) (map[string]File, error) {
 	return out, nil
 }
 
-func (fs *TorrentFs) rawOpen(path string) (File, error) {
-	files, err := fs.files()
+func (fs *TorrentFs) rawOpen(ctx context.Context, path string) (File, error) {
+	files, err := fs.files(ctx)
 	if err != nil {
 		return nil, err
 	}
@@ -183,8 +176,8 @@ func (fs *TorrentFs) rawOpen(path string) (File, error) {
 	return file, err
 }
 
-func (fs *TorrentFs) rawStat(filename string) (fs.FileInfo, error) {
-	files, err := fs.files()
+func (fs *TorrentFs) rawStat(ctx context.Context, filename string) (fs.FileInfo, error) {
+	files, err := fs.files(ctx)
 	if err != nil {
 		return nil, err
 	}
@@ -196,43 +189,43 @@ func (fs *TorrentFs) rawStat(filename string) (fs.FileInfo, error) {
 }
 
 // Stat implements Filesystem.
-func (fs *TorrentFs) Stat(filename string) (fs.FileInfo, error) {
+func (fs *TorrentFs) Stat(ctx context.Context, filename string) (fs.FileInfo, error) {
 	if filename == Separator {
 		return newDirInfo(filename), nil
 	}
 
-	fsPath, nestedFs, nestedFsPath, err := fs.resolver.resolvePath(filename, fs.rawOpen)
+	fsPath, nestedFs, nestedFsPath, err := fs.resolver.resolvePath(ctx, filename, fs.rawOpen)
 	if err != nil {
 		return nil, err
 	}
 	if nestedFs != nil {
-		return nestedFs.Stat(nestedFsPath)
+		return nestedFs.Stat(ctx, nestedFsPath)
 	}
 
-	return fs.rawStat(fsPath)
+	return fs.rawStat(ctx, fsPath)
 }
 
-func (fs *TorrentFs) Open(filename string) (File, error) {
-	fsPath, nestedFs, nestedFsPath, err := fs.resolver.resolvePath(filename, fs.rawOpen)
+func (fs *TorrentFs) Open(ctx context.Context, filename string) (File, error) {
+	fsPath, nestedFs, nestedFsPath, err := fs.resolver.resolvePath(ctx, filename, fs.rawOpen)
 	if err != nil {
 		return nil, err
 	}
 	if nestedFs != nil {
-		return nestedFs.Open(nestedFsPath)
+		return nestedFs.Open(ctx, nestedFsPath)
 	}
 
-	return fs.rawOpen(fsPath)
+	return fs.rawOpen(ctx, fsPath)
 }
 
-func (fs *TorrentFs) ReadDir(name string) ([]fs.DirEntry, error) {
-	fsPath, nestedFs, nestedFsPath, err := fs.resolver.resolvePath(name, fs.rawOpen)
+func (fs *TorrentFs) ReadDir(ctx context.Context, name string) ([]fs.DirEntry, error) {
+	fsPath, nestedFs, nestedFsPath, err := fs.resolver.resolvePath(ctx, name, fs.rawOpen)
 	if err != nil {
 		return nil, err
 	}
 	if nestedFs != nil {
-		return nestedFs.ReadDir(nestedFsPath)
+		return nestedFs.ReadDir(ctx, nestedFsPath)
 	}
-	files, err := fs.files()
+	files, err := fs.files(ctx)
 	if err != nil {
 		return nil, err
 	}
@@ -240,13 +233,13 @@ func (fs *TorrentFs) ReadDir(name string) ([]fs.DirEntry, error) {
 	return listDirFromFiles(files, fsPath)
 }
 
-func (fs *TorrentFs) Unlink(name string) error {
+func (fs *TorrentFs) Unlink(ctx context.Context, name string) error {
 	name = AbsPath(name)
 
 	fs.mu.Lock()
 	defer fs.mu.Unlock()
 
-	files, err := fs.files()
+	files, err := fs.files(ctx)
 	if err != nil {
 		return err
 	}
@@ -266,48 +259,84 @@ func (fs *TorrentFs) Unlink(name string) error {
 	return fs.Torrent.ExcludeFile(context.Background(), tfile.file)
 }
 
-type reader interface {
-	iio.Reader
-	missinggo.ReadContexter
+var _ File = &torrentFile{}
+
+type torrentFile struct {
+	name string
+
+	mu sync.Mutex
+
+	tr torrent.Reader
+
+	file *torrent.File
 }
 
-type readAtWrapper struct {
-	timeout int
-	mu      sync.Mutex
+func openTorrentFile(ctx context.Context, name string, file *torrent.File) (*torrentFile, error) {
+	select {
+	case <-file.Torrent().GotInfo():
+		break
+	case <-ctx.Done():
+		return nil, ctx.Err()
+	}
 
-	torrent.Reader
-	io.ReaderAt
-	io.Closer
+	r := file.NewReader()
+	r.SetReadahead(4096) // TODO configurable
+	r.SetResponsive()
+
+	return &torrentFile{
+		name: name,
+		tr:   r,
+		file: file,
+	}, nil
 }
 
-func newReadAtWrapper(r torrent.Reader, timeout int) reader {
-	w := &readAtWrapper{Reader: r, timeout: timeout}
-	w.SetResponsive()
-	return w
+func (tf *torrentFile) Stat() (fs.FileInfo, error) {
+	return newFileInfo(tf.name, tf.file.Length()), nil
 }
 
-func (rw *readAtWrapper) ReadAt(p []byte, off int64) (int, error) {
+func (tf *torrentFile) Size() int64 {
+	return tf.file.Length()
+}
+
+func (tf *torrentFile) IsDir() bool {
+	return false
+}
+
+func (rw *torrentFile) Close(ctx context.Context) error {
 	rw.mu.Lock()
 	defer rw.mu.Unlock()
-	_, err := rw.Seek(off, io.SeekStart)
+
+	return rw.tr.Close()
+}
+
+// Read implements ctxio.Reader.
+func (tf *torrentFile) Read(ctx context.Context, p []byte) (n int, err error) {
+	tf.mu.Lock()
+	defer tf.mu.Unlock()
+
+	return tf.tr.ReadContext(ctx, p)
+}
+
+func (yf *torrentFile) ReadAt(ctx context.Context, p []byte, off int64) (int, error) {
+	yf.mu.Lock()
+	defer yf.mu.Unlock()
+
+	_, err := yf.tr.Seek(off, io.SeekStart)
 	if err != nil {
 		return 0, err
 	}
 
-	return readAtLeast(rw, rw.timeout, p, len(p))
+	return readAtLeast(ctx, yf, p, len(p))
 }
 
-func readAtLeast(r missinggo.ReadContexter, timeout int, buf []byte, min int) (n int, err error) {
+func readAtLeast(ctx context.Context, r ctxio.Reader, buf []byte, min int) (n int, err error) {
 	if len(buf) < min {
 		return 0, io.ErrShortBuffer
 	}
 	for n < min && err == nil {
 		var nn int
 
-		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
-		defer cancel()
-
-		nn, err = r.ReadContext(ctx, buf[n:])
+		nn, err = r.Read(ctx, buf[n:])
 		n += nn
 	}
 	if n >= min {
@@ -317,63 +346,3 @@ func readAtLeast(r missinggo.ReadContexter, timeout int, buf []byte, min int) (n
 	}
 	return
 }
-
-func (rw *readAtWrapper) Close() error {
-	rw.mu.Lock()
-	defer rw.mu.Unlock()
-	return rw.Reader.Close()
-}
-
-var _ File = &torrentFile{}
-
-type torrentFile struct {
-	name string
-
-	reader  reader
-	timeout int
-
-	file *torrent.File
-}
-
-func (d *torrentFile) Stat() (fs.FileInfo, error) {
-	return newFileInfo(d.name, d.file.Length()), nil
-}
-
-func (d *torrentFile) load() {
-	if d.reader != nil {
-		return
-	}
-	d.reader = newReadAtWrapper(d.file.NewReader(), d.timeout)
-}
-
-func (d *torrentFile) Size() int64 {
-	return d.file.Length()
-}
-
-func (d *torrentFile) IsDir() bool {
-	return false
-}
-
-func (d *torrentFile) Close() error {
-	var err error
-	if d.reader != nil {
-		err = d.reader.Close()
-	}
-
-	d.reader = nil
-
-	return err
-}
-
-func (d *torrentFile) Read(p []byte) (n int, err error) {
-	d.load()
-	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(d.timeout)*time.Second)
-	defer cancel()
-
-	return d.reader.ReadContext(ctx, p)
-}
-
-func (d *torrentFile) ReadAt(p []byte, off int64) (n int, err error) {
-	d.load()
-	return d.reader.ReadAt(p, off)
-}
diff --git a/src/host/vfs/torrent_test.go b/src/host/vfs/torrent_test.go
index 2a8fac9..7de12ac 100644
--- a/src/host/vfs/torrent_test.go
+++ b/src/host/vfs/torrent_test.go
@@ -1,6 +1,7 @@
 package vfs
 
 import (
+	"context"
 	"os"
 	"testing"
 
@@ -87,6 +88,8 @@ func TestMain(m *testing.M) {
 func TestReadAtTorrent(t *testing.T) {
 	t.Parallel()
 
+	ctx := context.Background()
+
 	require := require.New(t)
 
 	to, err := Cli.AddMagnet(testMagnet)
@@ -96,19 +99,18 @@ func TestReadAtTorrent(t *testing.T) {
 	torrFile := to.Files()[0]
 
 	tf := torrentFile{
-		file:    torrFile,
-		timeout: 500,
+		file: torrFile,
 	}
 
-	defer tf.Close()
+	defer tf.Close(ctx)
 
 	toRead := make([]byte, 5)
-	n, err := tf.ReadAt(toRead, 6)
+	n, err := tf.ReadAt(ctx, toRead, 6)
 	require.NoError(err)
 	require.Equal(5, n)
 	require.Equal([]byte{0x0, 0x0, 0x1f, 0x76, 0x54}, toRead)
 
-	n, err = tf.ReadAt(toRead, 0)
+	n, err = tf.ReadAt(ctx, toRead, 0)
 	require.NoError(err)
 	require.Equal(5, n)
 	require.Equal([]byte{0x49, 0x44, 0x33, 0x3, 0x0}, toRead)
@@ -117,6 +119,8 @@ func TestReadAtTorrent(t *testing.T) {
 func TestReadAtWrapper(t *testing.T) {
 	t.Parallel()
 
+	ctx := context.Background()
+
 	require := require.New(t)
 
 	to, err := Cli.AddMagnet(testMagnet)
@@ -125,16 +129,16 @@ func TestReadAtWrapper(t *testing.T) {
 	<-to.GotInfo()
 	torrFile := to.Files()[0]
 
-	r := newReadAtWrapper(torrFile.NewReader(), 10)
-	defer r.Close()
+	r, err := openTorrentFile(ctx, "file", torrFile)
+	defer r.Close(ctx)
 
 	toRead := make([]byte, 5)
-	n, err := r.ReadAt(toRead, 6)
+	n, err := r.ReadAt(ctx, toRead, 6)
 	require.NoError(err)
 	require.Equal(5, n)
 	require.Equal([]byte{0x0, 0x0, 0x1f, 0x76, 0x54}, toRead)
 
-	n, err = r.ReadAt(toRead, 0)
+	n, err = r.ReadAt(ctx, toRead, 0)
 	require.NoError(err)
 	require.Equal(5, n)
 	require.Equal([]byte{0x49, 0x44, 0x33, 0x3, 0x0}, toRead)
diff --git a/src/iio/wrapper_test.go b/src/iio/wrapper_test.go
index 7d8a82d..e53471f 100644
--- a/src/iio/wrapper_test.go
+++ b/src/iio/wrapper_test.go
@@ -1,11 +1,12 @@
 package iio_test
 
 import (
+	"context"
 	"io"
 	"testing"
 
+	"git.kmsign.ru/royalcat/tstor/pkg/ctxio"
 	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
-	"git.kmsign.ru/royalcat/tstor/src/iio"
 	"github.com/stretchr/testify/require"
 )
 
@@ -14,11 +15,12 @@ var testData []byte = []byte("Hello World")
 func TestSeekerWrapper(t *testing.T) {
 	t.Parallel()
 
+	ctx := context.Background()
 	require := require.New(t)
 
 	mf := vfs.NewMemoryFile("text.txt", testData)
 
-	r := iio.NewSeekerWrapper(mf, mf.Size())
+	r := ctxio.IoReadSeekCloserWrapper(ctx, mf, mf.Size())
 	defer r.Close()
 
 	n, err := r.Seek(6, io.SeekStart)

From ef751771d2cfaff828b7f1bd758952a997ad01aa Mon Sep 17 00:00:00 2001
From: royalcat <k.adamovich20@gmail.com>
Date: Thu, 28 Mar 2024 16:09:42 +0300
Subject: [PATCH 17/18] update

---
 .golangci.yml                                 |   2 +-
 cmd/tstor/main.go                             |   8 +-
 go.mod                                        |  17 +-
 go.sum                                        |  37 +-
 graphql/mutation.graphql                      |  25 +-
 graphql/query.graphql                         |   7 +-
 pkg/ctxbilly/change.go                        |  27 +
 pkg/ctxbilly/fs.go                            |  92 +++
 pkg/ctxbilly/mem.go                           | 166 +++++
 pkg/ctxio/cachereader.go                      |  63 ++
 pkg/ctxio/copy.go                             |  89 +++
 pkg/ctxio/filebuffer.go                       | 180 +++++
 pkg/ctxio/io.go                               | 663 ++++++++++++++++++
 pkg/ctxio/reader.go                           |  40 +-
 pkg/ctxio/seeker.go                           |   4 +-
 pkg/ctxio/teereader.go                        |  20 +
 pkg/go-nfs/.github/dependabot.yml             |  11 +
 .../.github/workflows/codeql-analysis.yml     |  51 ++
 pkg/go-nfs/.github/workflows/go.yml           |  36 +
 pkg/go-nfs/CONTRIBUTING.md                    |  11 +
 pkg/go-nfs/LICENSE                            | 202 ++++++
 pkg/go-nfs/README.md                          |  96 +++
 pkg/go-nfs/SECURITY.md                        |  11 +
 pkg/go-nfs/capability_check.go                |   9 +
 pkg/go-nfs/conn.go                            | 335 +++++++++
 pkg/go-nfs/errors.go                          | 230 ++++++
 pkg/go-nfs/example/helloworld/main.go         |  52 ++
 pkg/go-nfs/example/osnfs/changeos.go          |  38 +
 pkg/go-nfs/example/osnfs/changeos_unix.go     |  28 +
 pkg/go-nfs/example/osnfs/main.go              |  36 +
 pkg/go-nfs/example/osview/main.go             |  37 +
 pkg/go-nfs/file.go                            | 377 ++++++++++
 pkg/go-nfs/file/file.go                       |  17 +
 pkg/go-nfs/file/file_unix.go                  |  24 +
 pkg/go-nfs/file/file_windows.go               |  12 +
 pkg/go-nfs/filesystem.go                      | 101 +++
 pkg/go-nfs/handler.go                         |  52 ++
 pkg/go-nfs/helpers/billlyfs.go                | 157 +++++
 .../go-nfs/helpers/cachinghandler.go          |  23 +-
 pkg/go-nfs/helpers/memfs/memfs.go             | 414 +++++++++++
 pkg/go-nfs/helpers/memfs/storage.go           | 243 +++++++
 pkg/go-nfs/helpers/nullauthhandler.go         |  59 ++
 pkg/go-nfs/log.go                             | 216 ++++++
 pkg/go-nfs/mount.go                           |  58 ++
 pkg/go-nfs/mountinterface.go                  |  90 +++
 pkg/go-nfs/nfs.go                             |  38 +
 pkg/go-nfs/nfs_onaccess.go                    |  45 ++
 pkg/go-nfs/nfs_oncommit.go                    |  51 ++
 pkg/go-nfs/nfs_oncreate.go                    | 125 ++++
 pkg/go-nfs/nfs_onfsinfo.go                    |  89 +++
 pkg/go-nfs/nfs_onfsstat.go                    |  59 ++
 pkg/go-nfs/nfs_ongetattr.go                   |  48 ++
 pkg/go-nfs/nfs_onlink.go                      |  94 +++
 pkg/go-nfs/nfs_onlookup.go                    |  86 +++
 pkg/go-nfs/nfs_onmkdir.go                     |  94 +++
 pkg/go-nfs/nfs_onmknod.go                     | 158 +++++
 pkg/go-nfs/nfs_onpathconf.go                  |  55 ++
 pkg/go-nfs/nfs_onread.go                      |  97 +++
 pkg/go-nfs/nfs_onreaddir.go                   | 195 ++++++
 pkg/go-nfs/nfs_onreaddirplus.go               | 153 ++++
 pkg/go-nfs/nfs_onreadlink.go                  |  55 ++
 pkg/go-nfs/nfs_onremove.go                    |  85 +++
 pkg/go-nfs/nfs_onrename.go                    | 120 ++++
 pkg/go-nfs/nfs_onrmdir.go                     |   9 +
 pkg/go-nfs/nfs_onsetattr.go                   |  80 +++
 pkg/go-nfs/nfs_onsymlink.go                   |  88 +++
 pkg/go-nfs/nfs_onwrite.go                     | 116 +++
 pkg/go-nfs/nfs_test.go                        | 293 ++++++++
 pkg/go-nfs/nfsinterface.go                    | 188 +++++
 pkg/go-nfs/server.go                          | 102 +++
 pkg/go-nfs/time.go                            |  32 +
 pkg/kvtrace/kvmetrics.go                      |  90 +++
 src/config/default.go                         |   7 +-
 src/config/load.go                            |   5 +-
 src/config/model.go                           |   7 +-
 src/delivery/graphql/generated.go             | 485 ++++++++++++-
 src/delivery/graphql/model/models_gen.go      |  10 +
 .../graphql/resolver/mutation.resolvers.go    |  23 +-
 .../graphql/resolver/query.resolvers.go       |  97 ++-
 src/delivery/http.go                          |   2 +-
 src/delivery/router.go                        |   7 +-
 src/export/httpfs/httpfs.go                   |   4 +-
 src/export/nfs/handler.go                     |  12 +-
 src/export/nfs/kvhandler.go                   | 127 ++++
 src/export/nfs/{wrapper-v3.go => wrapper.go}  | 122 ++--
 src/export/webdav/fs.go                       |   1 +
 src/host/datastorage/piece_storage.go         |   9 +-
 src/host/datastorage/setup.go                 |   2 +-
 src/host/datastorage/storage.go               | 300 +++++++-
 src/host/service/service.go                   | 152 ++--
 src/host/tkv/new.go                           |  21 +
 src/host/vfs/archive.go                       | 313 +++++----
 src/host/vfs/archive_test.go                  |  31 +-
 src/host/vfs/dir.go                           |  59 +-
 src/host/vfs/dummy.go                         | 125 ++++
 src/host/vfs/fs.go                            |  14 +-
 src/host/vfs/fs_test.go                       |   2 +-
 src/host/vfs/log.go                           | 195 +++++-
 src/host/vfs/memory.go                        |  42 +-
 src/host/vfs/os.go                            | 119 +---
 src/host/vfs/os_test.go                       |  75 ++
 src/host/vfs/resolver.go                      |  98 ++-
 src/host/vfs/resolver_test.go                 | 396 +++++------
 src/host/vfs/torrent.go                       | 208 +++++-
 src/host/vfs/torrent_test.go                  |  87 ++-
 src/host/vfs/utils.go                         |   7 +
 src/log/nfs.go                                |  10 +-
 107 files changed, 9435 insertions(+), 850 deletions(-)
 create mode 100644 pkg/ctxbilly/change.go
 create mode 100644 pkg/ctxbilly/fs.go
 create mode 100644 pkg/ctxbilly/mem.go
 create mode 100644 pkg/ctxio/cachereader.go
 create mode 100644 pkg/ctxio/copy.go
 create mode 100644 pkg/ctxio/filebuffer.go
 create mode 100644 pkg/ctxio/io.go
 create mode 100644 pkg/ctxio/teereader.go
 create mode 100644 pkg/go-nfs/.github/dependabot.yml
 create mode 100644 pkg/go-nfs/.github/workflows/codeql-analysis.yml
 create mode 100644 pkg/go-nfs/.github/workflows/go.yml
 create mode 100644 pkg/go-nfs/CONTRIBUTING.md
 create mode 100644 pkg/go-nfs/LICENSE
 create mode 100644 pkg/go-nfs/README.md
 create mode 100644 pkg/go-nfs/SECURITY.md
 create mode 100644 pkg/go-nfs/capability_check.go
 create mode 100644 pkg/go-nfs/conn.go
 create mode 100644 pkg/go-nfs/errors.go
 create mode 100644 pkg/go-nfs/example/helloworld/main.go
 create mode 100644 pkg/go-nfs/example/osnfs/changeos.go
 create mode 100644 pkg/go-nfs/example/osnfs/changeos_unix.go
 create mode 100644 pkg/go-nfs/example/osnfs/main.go
 create mode 100644 pkg/go-nfs/example/osview/main.go
 create mode 100644 pkg/go-nfs/file.go
 create mode 100644 pkg/go-nfs/file/file.go
 create mode 100644 pkg/go-nfs/file/file_unix.go
 create mode 100644 pkg/go-nfs/file/file_windows.go
 create mode 100644 pkg/go-nfs/filesystem.go
 create mode 100644 pkg/go-nfs/handler.go
 create mode 100644 pkg/go-nfs/helpers/billlyfs.go
 rename src/export/nfs/cache.go => pkg/go-nfs/helpers/cachinghandler.go (90%)
 create mode 100644 pkg/go-nfs/helpers/memfs/memfs.go
 create mode 100644 pkg/go-nfs/helpers/memfs/storage.go
 create mode 100644 pkg/go-nfs/helpers/nullauthhandler.go
 create mode 100644 pkg/go-nfs/log.go
 create mode 100644 pkg/go-nfs/mount.go
 create mode 100644 pkg/go-nfs/mountinterface.go
 create mode 100644 pkg/go-nfs/nfs.go
 create mode 100644 pkg/go-nfs/nfs_onaccess.go
 create mode 100644 pkg/go-nfs/nfs_oncommit.go
 create mode 100644 pkg/go-nfs/nfs_oncreate.go
 create mode 100644 pkg/go-nfs/nfs_onfsinfo.go
 create mode 100644 pkg/go-nfs/nfs_onfsstat.go
 create mode 100644 pkg/go-nfs/nfs_ongetattr.go
 create mode 100644 pkg/go-nfs/nfs_onlink.go
 create mode 100644 pkg/go-nfs/nfs_onlookup.go
 create mode 100644 pkg/go-nfs/nfs_onmkdir.go
 create mode 100644 pkg/go-nfs/nfs_onmknod.go
 create mode 100644 pkg/go-nfs/nfs_onpathconf.go
 create mode 100644 pkg/go-nfs/nfs_onread.go
 create mode 100644 pkg/go-nfs/nfs_onreaddir.go
 create mode 100644 pkg/go-nfs/nfs_onreaddirplus.go
 create mode 100644 pkg/go-nfs/nfs_onreadlink.go
 create mode 100644 pkg/go-nfs/nfs_onremove.go
 create mode 100644 pkg/go-nfs/nfs_onrename.go
 create mode 100644 pkg/go-nfs/nfs_onrmdir.go
 create mode 100644 pkg/go-nfs/nfs_onsetattr.go
 create mode 100644 pkg/go-nfs/nfs_onsymlink.go
 create mode 100644 pkg/go-nfs/nfs_onwrite.go
 create mode 100644 pkg/go-nfs/nfs_test.go
 create mode 100644 pkg/go-nfs/nfsinterface.go
 create mode 100644 pkg/go-nfs/server.go
 create mode 100644 pkg/go-nfs/time.go
 create mode 100644 pkg/kvtrace/kvmetrics.go
 create mode 100644 src/export/nfs/kvhandler.go
 rename src/export/nfs/{wrapper-v3.go => wrapper.go} (52%)
 create mode 100644 src/host/tkv/new.go
 create mode 100644 src/host/vfs/dummy.go
 create mode 100644 src/host/vfs/os_test.go

diff --git a/.golangci.yml b/.golangci.yml
index d7760dc..8d56cb2 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -19,7 +19,7 @@ linters:
     - asciicheck
     - bidichk
     - bodyclose
-    # - containedctx
+    - containedctx
     - durationcheck
     - errcheck
     - nakedret
diff --git a/cmd/tstor/main.go b/cmd/tstor/main.go
index f36f761..b430181 100644
--- a/cmd/tstor/main.go
+++ b/cmd/tstor/main.go
@@ -3,7 +3,6 @@ package main
 import (
 	"context"
 	"fmt"
-	"log/slog"
 
 	"net"
 	nethttp "net/http"
@@ -14,6 +13,8 @@ import (
 	"syscall"
 	"time"
 
+	wnfs "git.kmsign.ru/royalcat/tstor/pkg/go-nfs"
+	"git.kmsign.ru/royalcat/tstor/pkg/rlog"
 	"git.kmsign.ru/royalcat/tstor/src/config"
 	"git.kmsign.ru/royalcat/tstor/src/delivery"
 	"git.kmsign.ru/royalcat/tstor/src/host"
@@ -23,7 +24,6 @@ import (
 	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
 	"git.kmsign.ru/royalcat/tstor/src/telemetry"
 	"github.com/urfave/cli/v2"
-	wnfs "github.com/willscott/go-nfs"
 
 	_ "git.kmsign.ru/royalcat/tstor/pkg/rlog"
 	"git.kmsign.ru/royalcat/tstor/src/export/fuse"
@@ -79,7 +79,7 @@ func run(configPath string) error {
 		defer client.Shutdown(ctx)
 	}
 
-	log := slog.Default().With("component", "run")
+	log := rlog.ComponentLog("run")
 
 	// TODO make optional
 	err = syscall.Setpriority(syscall.PRIO_PGRP, 0, 19)
@@ -137,7 +137,7 @@ func run(configPath string) error {
 		return fmt.Errorf("error creating data folder: %w", err)
 	}
 	sfs := host.NewTorrentStorage(conf.SourceDir, ts)
-	sfs = vfs.WrapLogFS(sfs, slog.Default())
+	sfs = vfs.WrapLogFS(sfs)
 
 	// TODO make separate function
 	// {
diff --git a/go.mod b/go.mod
index e39f509..6cbec8e 100644
--- a/go.mod
+++ b/go.mod
@@ -14,6 +14,7 @@ require (
 	github.com/bodgit/sevenzip v1.4.5
 	github.com/dgraph-io/badger/v4 v4.2.0
 	github.com/dgraph-io/ristretto v0.1.1
+	github.com/dustin/go-humanize v1.0.0
 	github.com/gin-contrib/pprof v1.4.0
 	github.com/gin-gonic/gin v1.9.1
 	github.com/go-git/go-billy/v5 v5.5.0
@@ -27,8 +28,9 @@ require (
 	github.com/knadh/koanf/providers/structs v0.1.0
 	github.com/knadh/koanf/v2 v2.0.1
 	github.com/nwaples/rardecode/v2 v2.0.0-beta.2
+	github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93
 	github.com/ravilushqa/otelgqlgen v0.15.0
-	github.com/royalcat/kv v0.0.0-20240318203654-181645f85b10
+	github.com/royalcat/kv v0.0.0-20240327213417-8cf5696b2389
 	github.com/rs/zerolog v1.32.0
 	github.com/samber/slog-multi v1.0.2
 	github.com/samber/slog-zerolog v1.0.0
@@ -36,7 +38,8 @@ require (
 	github.com/stretchr/testify v1.8.4
 	github.com/urfave/cli/v2 v2.27.0
 	github.com/vektah/gqlparser/v2 v2.5.11
-	github.com/willscott/go-nfs v0.0.2
+	github.com/willscott/go-nfs-client v0.0.0-20240104095149-b44639837b00
+	github.com/willscott/memphis v0.0.0-20210922141505-529d4987ab7e
 	go.opentelemetry.io/otel v1.24.0
 	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0
 	go.opentelemetry.io/otel/exporters/prometheus v0.46.0
@@ -46,6 +49,7 @@ require (
 	go.uber.org/multierr v1.11.0
 	golang.org/x/exp v0.0.0-20231226003508-02704c960a9b
 	golang.org/x/net v0.19.0
+	golang.org/x/sys v0.17.0
 )
 
 require (
@@ -79,8 +83,8 @@ require (
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 	github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
 	github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
+	github.com/cyphar/filepath-securejoin v0.2.4 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/dustin/go-humanize v1.0.0 // indirect
 	github.com/edsrzf/mmap-go v1.1.0 // indirect
 	github.com/fatih/structs v1.1.0 // indirect
 	github.com/fsnotify/fsnotify v1.6.0 // indirect
@@ -141,11 +145,13 @@ require (
 	github.com/pion/webrtc/v3 v3.1.42 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/polydawn/go-timeless-api v0.0.0-20220821201550-b93919e12c56 // indirect
+	github.com/polydawn/refmt v0.0.0-20201211092308-30ac6d18308e // indirect
+	github.com/polydawn/rio v0.0.0-20220823181337-7c31ad9831a4 // indirect
 	github.com/prometheus/client_golang v1.18.0 // indirect
 	github.com/prometheus/client_model v0.6.0 // indirect
 	github.com/prometheus/common v0.45.0 // indirect
 	github.com/prometheus/procfs v0.12.0 // indirect
-	github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93 // indirect
 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
 	github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
@@ -156,7 +162,7 @@ require (
 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
 	github.com/ugorji/go/codec v1.2.11 // indirect
 	github.com/ulikunitz/xz v0.5.11 // indirect
-	github.com/willscott/go-nfs-client v0.0.0-20240104095149-b44639837b00 // indirect
+	github.com/warpfork/go-errcat v0.0.0-20180917083543-335044ffc86e // indirect
 	github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
 	go.etcd.io/bbolt v1.3.6 // indirect
 	go.opencensus.io v0.24.0 // indirect
@@ -169,7 +175,6 @@ require (
 	golang.org/x/crypto v0.17.0 // indirect
 	golang.org/x/mod v0.14.0 // indirect
 	golang.org/x/sync v0.5.0 // indirect
-	golang.org/x/sys v0.17.0 // indirect
 	golang.org/x/text v0.14.0 // indirect
 	golang.org/x/time v0.5.0 // indirect
 	golang.org/x/tools v0.16.0 // indirect
diff --git a/go.sum b/go.sum
index f56ac68..2cf862e 100644
--- a/go.sum
+++ b/go.sum
@@ -159,6 +159,8 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
 github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
 github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
+github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -207,6 +209,7 @@ github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a/go.mod
 github.com/glycerine/goconvey v0.0.0-20180728074245-46e3a41ad493/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
 github.com/glycerine/goconvey v0.0.0-20190315024820-982ee783a72e/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
 github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
+github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
 github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
 github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
@@ -310,6 +313,7 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gopherjs/gopherjs v0.0.0-20190309154008-847fc94819f9/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99 h1:twflg0XRTjwKpxb/jFExr4HGq6on2dEOmnL6FV+fgPw=
 github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
 github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
@@ -342,6 +346,7 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
 github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@@ -405,6 +410,7 @@ github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE
 github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
 github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
 github.com/nwaples/rardecode/v2 v2.0.0-beta.2 h1:e3mzJFJs4k83GXBEiTaQ5HgSc/kOK8q0rDaRO0MPaOk=
 github.com/nwaples/rardecode/v2 v2.0.0-beta.2/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY=
 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
@@ -418,6 +424,8 @@ github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
 github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
+github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
+github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
 github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
 github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
 github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
@@ -476,6 +484,15 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/polydawn/go-timeless-api v0.0.0-20201121022836-7399661094a6/go.mod h1:z2fMUifgtqrZiNLgzF4ZR8pX+YFLCmAp1jJTSTvyDMM=
+github.com/polydawn/go-timeless-api v0.0.0-20220821201550-b93919e12c56 h1:LQ103HjiN76aqIxnQNgdZ+7NveuKd45+Q+TYGJVVsyw=
+github.com/polydawn/go-timeless-api v0.0.0-20220821201550-b93919e12c56/go.mod h1:OAK6p/pJUakz6jQ+HlSw16gVMnuohxqJFGoypUYyr4w=
+github.com/polydawn/refmt v0.0.0-20190807091052-3d65705ee9f1/go.mod h1:uIp+gprXxxrWSjjklXD+mN4wed/tMfjMMmN/9+JsA9o=
+github.com/polydawn/refmt v0.0.0-20201211092308-30ac6d18308e h1:ZOcivgkkFRnjfoTcGsDq3UQYiBmekwLA+qg0OjyB/ls=
+github.com/polydawn/refmt v0.0.0-20201211092308-30ac6d18308e/go.mod h1:uIp+gprXxxrWSjjklXD+mN4wed/tMfjMMmN/9+JsA9o=
+github.com/polydawn/rio v0.0.0-20201122020833-6192319df581/go.mod h1:mwZtAu36D3fSNzVLN1we6PFdRU4VeE+RXLTZiOiQlJ0=
+github.com/polydawn/rio v0.0.0-20220823181337-7c31ad9831a4 h1:SNhgcsCNGEqz7Tp46YHEvcjF1s5x+ZGWcVzFoghkuMA=
+github.com/polydawn/rio v0.0.0-20220823181337-7c31ad9831a4/go.mod h1:fZ8OGW5CVjZHyQeNs8QH3X3tUxrPcx1jxHSl2z6Xv00=
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
 github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
 github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
@@ -514,8 +531,8 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE
 github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
 github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
 github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
-github.com/royalcat/kv v0.0.0-20240318203654-181645f85b10 h1:8vwpCzvVqzNzkYRH9kA3GV5fkWs+8s0jdxtGvswL/MU=
-github.com/royalcat/kv v0.0.0-20240318203654-181645f85b10/go.mod h1:Ff0Z/r1H3ojacpEe8SashMKJx6YCIhWrYtpdV8Y/k3A=
+github.com/royalcat/kv v0.0.0-20240327213417-8cf5696b2389 h1:7XbHzr1TOaxs5Y/i9GtTEOOSTzfQ4ESYqF38DVfPkFY=
+github.com/royalcat/kv v0.0.0-20240327213417-8cf5696b2389/go.mod h1:Ff0Z/r1H3ojacpEe8SashMKJx6YCIhWrYtpdV8Y/k3A=
 github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 h1:Lt9DzQALzHoDwMBGJ6v8ObDPR0dzr2a6sXTB1Fq7IHs=
 github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA=
 github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
@@ -541,8 +558,13 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
+github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
 github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
 github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs=
+github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
+github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
 github.com/sosodev/duration v1.2.0 h1:pqK/FLSjsAADWY74SyWDCjOcd5l7H8GSnnOGEB9A1Us=
 github.com/sosodev/duration v1.2.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
@@ -582,12 +604,16 @@ github.com/urfave/cli/v2 v2.27.0 h1:uNs1K8JwTFL84X68j5Fjny6hfANh9nTlJ6dRtZAFAHY=
 github.com/urfave/cli/v2 v2.27.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
 github.com/vektah/gqlparser/v2 v2.5.11 h1:JJxLtXIoN7+3x6MBdtIP59TP1RANnY7pXOaDnADQSf8=
 github.com/vektah/gqlparser/v2 v2.5.11/go.mod h1:1rCcfwB2ekJofmluGWXMSEnPMZgbxzwj6FaZ/4OT8Cc=
+github.com/warpfork/go-errcat v0.0.0-20180917083543-335044ffc86e h1:FIB2fi7XJGHIdf5rWNsfFQqatIKxutT45G+wNuMQNgs=
+github.com/warpfork/go-errcat v0.0.0-20180917083543-335044ffc86e/go.mod h1:/qe02xr3jvTUz8u/PV0FHGpP8t96OQNP7U9BJMwMLEw=
+github.com/warpfork/go-wish v0.0.0-20200122115046-b9ea61034e4a h1:G++j5e0OC488te356JvdhaM8YS6nMsjLAYF7JxCv07w=
+github.com/warpfork/go-wish v0.0.0-20200122115046-b9ea61034e4a/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
 github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
 github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
-github.com/willscott/go-nfs v0.0.2 h1:BaBp1CpGDMooCT6bCgX6h6ZwgPcTMST4yToYZ9byee0=
-github.com/willscott/go-nfs v0.0.2/go.mod h1:SvullWeHxr/924WQNbUaZqtluBt2vuZ61g6yAV+xj7w=
 github.com/willscott/go-nfs-client v0.0.0-20240104095149-b44639837b00 h1:U0DnHRZFzoIV1oFEZczg5XyPut9yxk9jjtax/9Bxr/o=
 github.com/willscott/go-nfs-client v0.0.0-20240104095149-b44639837b00/go.mod h1:Tq++Lr/FgiS3X48q5FETemXiSLGuYMQT2sPjYNPJSwA=
+github.com/willscott/memphis v0.0.0-20210922141505-529d4987ab7e h1:1eHCP4w7tMmpfFBdrd5ff+vYU9THtrtA1yM9f0TLlJw=
+github.com/willscott/memphis v0.0.0-20210922141505-529d4987ab7e/go.mod h1:59vHBW4EpjiL5oiqgCrBp1Tc9JXRzKCNMEOaGmNfSHo=
 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -753,6 +779,7 @@ golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -807,6 +834,7 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3
 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
@@ -899,6 +927,7 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
diff --git a/graphql/mutation.graphql b/graphql/mutation.graphql
index 80830ed..0f45036 100644
--- a/graphql/mutation.graphql
+++ b/graphql/mutation.graphql
@@ -1,20 +1,25 @@
 type Mutation {
-    validateTorrents(filter: TorrentFilter!): Boolean!
-    cleanupTorrents(files: Boolean, dryRun: Boolean!): Int!
-    downloadTorrent(infohash: String!, file: String): DownloadTorrentResponse
+  validateTorrents(filter: TorrentFilter!): Boolean!
+  cleanupTorrents(files: Boolean, dryRun: Boolean!): CleanupResponse!
+  downloadTorrent(infohash: String!, file: String): DownloadTorrentResponse
+  dedupeStorage: Int!
 }
 
-
 input TorrentFilter @oneOf {
-    everything: Boolean
-    infohash: String
-    # pathGlob: String!
+  everything: Boolean
+  infohash: String
+  # pathGlob: String!
 }
 
 type DownloadTorrentResponse {
-    task: Task
+  task: Task
+}
+
+type CleanupResponse {
+  count: Int!
+  list: [String!]!
 }
 
 type Task {
-    id: ID!
-}
\ No newline at end of file
+  id: ID!
+}
diff --git a/graphql/query.graphql b/graphql/query.graphql
index aab94cb..8161a9f 100644
--- a/graphql/query.graphql
+++ b/graphql/query.graphql
@@ -1,6 +1,6 @@
 type Query {
   torrents(filter: TorrentsFilter, pagination: Pagination): [Torrent!]!
-  fsListDir(path: String!): [DirEntry!]!
+  fsListDir(path: String!): ListDirResponse!
 }
 
 input TorrentsFilter {
@@ -11,6 +11,11 @@ input TorrentsFilter {
   peersCount: IntFilter
 }
 
+type ListDirResponse {
+  root: DirEntry!
+  entries: [DirEntry!]!
+}
+
 input Pagination {
   offset: Int!
   limit: Int!
diff --git a/pkg/ctxbilly/change.go b/pkg/ctxbilly/change.go
new file mode 100644
index 0000000..e9a5105
--- /dev/null
+++ b/pkg/ctxbilly/change.go
@@ -0,0 +1,27 @@
+package ctxbilly
+
+import (
+	"context"
+	"os"
+	"time"
+)
+
+// Change abstract the FileInfo change related operations in a storage-agnostic
+// interface as an extension to the Basic interface
+type Change interface {
+	// Chmod changes the mode of the named file to mode. If the file is a
+	// symbolic link, it changes the mode of the link's target.
+	Chmod(ctx context.Context, name string, mode os.FileMode) error
+	// Lchown changes the numeric uid and gid of the named file. If the file is
+	// a symbolic link, it changes the uid and gid of the link itself.
+	Lchown(ctx context.Context, name string, uid, gid int) error
+	// Chown changes the numeric uid and gid of the named file. If the file is a
+	// symbolic link, it changes the uid and gid of the link's target.
+	Chown(ctx context.Context, name string, uid, gid int) error
+	// Chtimes changes the access and modification times of the named file,
+	// similar to the Unix utime() or utimes() functions.
+	//
+	// The underlying filesystem may truncate or round the values to a less
+	// precise time unit.
+	Chtimes(ctx context.Context, name string, atime time.Time, mtime time.Time) error
+}
diff --git a/pkg/ctxbilly/fs.go b/pkg/ctxbilly/fs.go
new file mode 100644
index 0000000..e14835b
--- /dev/null
+++ b/pkg/ctxbilly/fs.go
@@ -0,0 +1,92 @@
+package ctxbilly
+
+import (
+	"context"
+	"io"
+	"os"
+
+	"git.kmsign.ru/royalcat/tstor/pkg/ctxio"
+)
+
+type Filesystem interface {
+	// Create creates the named file with mode 0666 (before umask), truncating
+	// it if it already exists. If successful, methods on the returned File can
+	// be used for I/O; the associated file descriptor has mode O_RDWR.
+	Create(ctx context.Context, filename string) (File, error)
+	// Open opens the named file for reading. If successful, methods on the
+	// returned file can be used for reading; the associated file descriptor has
+	// mode O_RDONLY.
+	Open(ctx context.Context, filename string) (File, error)
+	// OpenFile is the generalized open call; most users will use Open or Create
+	// instead. It opens the named file with specified flag (O_RDONLY etc.) and
+	// perm, (0666 etc.) if applicable. If successful, methods on the returned
+	// File can be used for I/O.
+	OpenFile(ctx context.Context, filename string, flag int, perm os.FileMode) (File, error)
+	// Stat returns a FileInfo describing the named file.
+	Stat(ctx context.Context, filename string) (os.FileInfo, error)
+	// Rename renames (moves) oldpath to newpath. If newpath already exists and
+	// is not a directory, Rename replaces it. OS-specific restrictions may
+	// apply when oldpath and newpath are in different directories.
+	Rename(ctx context.Context, oldpath, newpath string) error
+	// Remove removes the named file or directory.
+	Remove(ctx context.Context, filename string) error
+	// Join joins any number of path elements into a single path, adding a
+	// Separator if necessary. Join calls filepath.Clean on the result; in
+	// particular, all empty strings are ignored. On Windows, the result is a
+	// UNC path if and only if the first path element is a UNC path.
+	Join(elem ...string) string
+
+	// TempFile creates a new temporary file in the directory dir with a name
+	// beginning with prefix, opens the file for reading and writing, and
+	// returns the resulting *os.File. If dir is the empty string, TempFile
+	// uses the default directory for temporary files (see os.TempDir).
+	// Multiple programs calling TempFile simultaneously will not choose the
+	// same file. The caller can use f.Name() to find the pathname of the file.
+	// It is the caller's responsibility to remove the file when no longer
+	// needed.
+	TempFile(ctx context.Context, dir, prefix string) (File, error)
+
+	// ReadDir reads the directory named by d(irname and returns a list of
+	// directory entries sorted by filename.
+	ReadDir(ctx context.Context, path string) ([]os.FileInfo, error)
+	// MkdirAll creates a directory named path, along with any necessary
+	// parents, and returns nil, or else returns an error. The permission bits
+	// perm are used for all directories that MkdirAll creates. If path is/
+	// already a directory, MkdirAll does nothing and returns nil.
+	MkdirAll(ctx context.Context, filename string, perm os.FileMode) error
+
+	// Lstat returns a FileInfo describing the named file. If the file is a
+	// symbolic link, the returned FileInfo describes the symbolic link. Lstat
+	// makes no attempt to follow the link.
+	Lstat(ctx context.Context, filename string) (os.FileInfo, error)
+	// Symlink creates a symbolic-link from link to target. target may be an
+	// absolute or relative path, and need not refer to an existing node.
+	// Parent directories of link are created as necessary.
+	Symlink(ctx context.Context, target, link string) error
+	// Readlink returns the target path of link.
+	Readlink(ctx context.Context, link string) (string, error)
+
+	// // Chroot returns a new filesystem from the same type where the new root is
+	// // the given path. Files outside of the designated directory tree cannot be
+	// // accessed.
+	// Chroot(path string) (Filesystem, error)
+	// // Root returns the root path of the filesystem.
+	// Root() string
+}
+
+type File interface {
+	// Name returns the name of the file as presented to Open.
+	Name() string
+	ctxio.Writer
+	ctxio.Reader
+	ctxio.ReaderAt
+	io.Seeker
+	ctxio.Closer
+	// Lock locks the file like e.g. flock. It protects against access from
+	// other processes.
+	Lock() error
+	// Unlock unlocks the file.
+	Unlock() error
+	// Truncate the file.
+	Truncate(ctx context.Context, size int64) error
+}
diff --git a/pkg/ctxbilly/mem.go b/pkg/ctxbilly/mem.go
new file mode 100644
index 0000000..934e18a
--- /dev/null
+++ b/pkg/ctxbilly/mem.go
@@ -0,0 +1,166 @@
+package ctxbilly
+
+import (
+	"context"
+	"io/fs"
+
+	"github.com/go-git/go-billy/v5"
+)
+
+func WrapFileSystem(bf billy.Filesystem) Filesystem {
+	return &wrapFS{
+		Filesystem: bf,
+	}
+}
+
+type wrapFS struct {
+	billy.Filesystem
+}
+
+var _ Filesystem = (*wrapFS)(nil)
+
+// Create implements Filesystem.
+// Subtle: this method shadows the method (Filesystem).Create of MemFS.Filesystem.
+func (m *wrapFS) Create(ctx context.Context, filename string) (File, error) {
+	bf, err := m.Filesystem.Create(filename)
+	if err != nil {
+		return nil, err
+	}
+	return &wrapFile{bf}, nil
+}
+
+// Lstat implements Filesystem.
+// Subtle: this method shadows the method (Filesystem).Lstat of MemFS.Filesystem.
+func (m *wrapFS) Lstat(ctx context.Context, filename string) (fs.FileInfo, error) {
+	return m.Filesystem.Lstat(filename)
+}
+
+// MkdirAll implements Filesystem.
+// Subtle: this method shadows the method (Filesystem).MkdirAll of MemFS.Filesystem.
+func (m *wrapFS) MkdirAll(ctx context.Context, filename string, perm fs.FileMode) error {
+	return m.Filesystem.MkdirAll(filename, perm)
+}
+
+// Open implements Filesystem.
+// Subtle: this method shadows the method (Filesystem).Open of MemFS.Filesystem.
+func (m *wrapFS) Open(ctx context.Context, filename string) (File, error) {
+	bf, err := m.Filesystem.Open(filename)
+	if err != nil {
+		return nil, err
+	}
+	return WrapFile(bf), nil
+}
+
+// OpenFile implements Filesystem.
+// Subtle: this method shadows the method (Filesystem).OpenFile of MemFS.Filesystem.
+func (m *wrapFS) OpenFile(ctx context.Context, filename string, flag int, perm fs.FileMode) (File, error) {
+	bf, err := m.Filesystem.OpenFile(filename, flag, perm)
+	if err != nil {
+		return nil, err
+	}
+	return WrapFile(bf), nil
+}
+
+// ReadDir implements Filesystem.
+// Subtle: this method shadows the method (Filesystem).ReadDir of MemFS.Filesystem.
+func (m *wrapFS) ReadDir(ctx context.Context, path string) ([]fs.FileInfo, error) {
+	return m.Filesystem.ReadDir(path)
+}
+
+// Readlink implements Filesystem.
+// Subtle: this method shadows the method (Filesystem).Readlink of MemFS.Filesystem.
+func (m *wrapFS) Readlink(ctx context.Context, link string) (string, error) {
+	return m.Filesystem.Readlink(link)
+}
+
+// Remove implements Filesystem.
+// Subtle: this method shadows the method (Filesystem).Remove of MemFS.Filesystem.
+func (m *wrapFS) Remove(ctx context.Context, filename string) error {
+	return m.Filesystem.Remove(filename)
+}
+
+// Rename implements Filesystem.
+// Subtle: this method shadows the method (Filesystem).Rename of MemFS.Filesystem.
+func (m *wrapFS) Rename(ctx context.Context, oldpath string, newpath string) error {
+	return m.Filesystem.Rename(oldpath, newpath)
+}
+
+// Stat implements Filesystem.
+// Subtle: this method shadows the method (Filesystem).Stat of MemFS.Filesystem.
+func (m *wrapFS) Stat(ctx context.Context, filename string) (fs.FileInfo, error) {
+	return m.Filesystem.Stat(filename)
+}
+
+// Symlink implements Filesystem.
+// Subtle: this method shadows the method (Filesystem).Symlink of MemFS.Filesystem.
+func (m *wrapFS) Symlink(ctx context.Context, target string, link string) error {
+	return m.Filesystem.Symlink(target, link)
+}
+
+// TempFile implements Filesystem.
+// Subtle: this method shadows the method (Filesystem).TempFile of MemFS.Filesystem.
+func (m *wrapFS) TempFile(ctx context.Context, dir string, prefix string) (File, error) {
+	file, err := m.Filesystem.TempFile(dir, prefix)
+	if err != nil {
+		return nil, err
+	}
+	return WrapFile(file), nil
+}
+
+func WrapFile(bf billy.File) File {
+	return &wrapFile{File: bf}
+}
+
+type wrapFile struct {
+	billy.File
+}
+
+var _ File = (*wrapFile)(nil)
+
+// Close implements File.
+// Subtle: this method shadows the method (File).Close of MemFile.File.
+func (m *wrapFile) Close(ctx context.Context) error {
+	return m.File.Close()
+}
+
+// Lock implements File.
+// Subtle: this method shadows the method (File).Lock of MemFile.File.
+func (m *wrapFile) Lock() error {
+	return m.File.Lock()
+}
+
+// Name implements File.
+// Subtle: this method shadows the method (File).Name of MemFile.File.
+func (m *wrapFile) Name() string {
+	return m.File.Name()
+}
+
+// Truncate implements File.
+// Subtle: this method shadows the method (File).Truncate of memFile.File.
+func (m *wrapFile) Truncate(ctx context.Context, size int64) error {
+	return m.File.Truncate(size)
+}
+
+// Read implements File.
+// Subtle: this method shadows the method (File).Read of MemFile.File.
+func (m *wrapFile) Read(ctx context.Context, p []byte) (n int, err error) {
+	return m.File.Read(p)
+}
+
+// ReadAt implements File.
+// Subtle: this method shadows the method (File).ReadAt of MemFile.File.
+func (m *wrapFile) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) {
+	return m.File.ReadAt(p, off)
+}
+
+// Unlock implements File.
+// Subtle: this method shadows the method (File).Unlock of MemFile.File.
+func (m *wrapFile) Unlock() error {
+	return m.File.Unlock()
+}
+
+// Write implements File.
+// Subtle: this method shadows the method (File).Write of MemFile.File.
+func (m *wrapFile) Write(ctx context.Context, p []byte) (n int, err error) {
+	return m.File.Write(p)
+}
diff --git a/pkg/ctxio/cachereader.go b/pkg/ctxio/cachereader.go
new file mode 100644
index 0000000..14e70f9
--- /dev/null
+++ b/pkg/ctxio/cachereader.go
@@ -0,0 +1,63 @@
+package ctxio
+
+import (
+	"context"
+	"errors"
+	"io"
+	"sync"
+)
+
+type CacheReader struct {
+	m sync.Mutex
+
+	fo int64
+	fr *FileBuffer
+	to int64
+	tr Reader
+}
+
+var _ FileReader = (*CacheReader)(nil)
+
+func NewCacheReader(r Reader) (FileReader, error) {
+	fr := NewFileBuffer(nil)
+	tr := TeeReader(r, fr)
+	return &CacheReader{fr: fr, tr: tr}, nil
+}
+
+func (dtr *CacheReader) ReadAt(ctx context.Context, p []byte, off int64) (int, error) {
+	dtr.m.Lock()
+	defer dtr.m.Unlock()
+	tb := off + int64(len(p))
+
+	if tb > dtr.fo {
+		w, err := CopyN(ctx, Discard, dtr.tr, tb-dtr.fo)
+		dtr.to += w
+		if err != nil && err != io.EOF {
+			return 0, err
+		}
+	}
+
+	n, err := dtr.fr.ReadAt(ctx, p, off)
+	dtr.fo += int64(n)
+	return n, err
+}
+
+func (dtr *CacheReader) Read(ctx context.Context, p []byte) (n int, err error) {
+	dtr.m.Lock()
+	defer dtr.m.Unlock()
+	// use directly tee reader here
+	n, err = dtr.tr.Read(ctx, p)
+	dtr.to += int64(n)
+	return
+}
+
+func (dtr *CacheReader) Close(ctx context.Context) error {
+	frcloser := dtr.fr.Close(ctx)
+
+	var closeerr error
+	if rc, ok := dtr.tr.(ReadCloser); ok {
+		closeerr = rc.Close(ctx)
+	}
+
+	return errors.Join(frcloser, closeerr)
+}
diff --git a/pkg/ctxio/copy.go b/pkg/ctxio/copy.go
new file mode 100644
index 0000000..4c98a8e
--- /dev/null
+++ b/pkg/ctxio/copy.go
@@ -0,0 +1,89 @@
+package ctxio
+
+// // CopyN copies n bytes (or until an error) from src to dst.
+// // It returns the number of bytes copied and the earliest
+// // error encountered while copying.
+// // On return, written == n if and only if err == nil.
+// //
+// // If dst implements [ReaderFrom], the copy is implemented using it.
+// func CopyN(ctx context.Context, dst Writer, src Reader, n int64) (written int64, err error) {
+// 	written, err = Copy(ctx, dst, LimitReader(src, n))
+// 	if written == n {
+// 		return n, nil
+// 	}
+// 	if written < n && err == nil {
+// 		// src stopped early; must have been EOF.
+// 		err = io.EOF
+// 	}
+
+// 	return
+// }
+
+// // Copy copies from src to dst until either EOF is reached
+// // on src or an error occurs. It returns the number of bytes
+// // copied and the first error encountered while copying, if any.
+// //
+// // A successful Copy returns err == nil, not err == EOF.
+// // Because Copy is defined to read from src until EOF, it does
+// // not treat an EOF from Read as an error to be reported.
+// //
+// // If src implements [WriterTo],
+// // the copy is implemented by calling src.WriteTo(dst).
+// // Otherwise, if dst implements [ReaderFrom],
+// // the copy is implemented by calling dst.ReadFrom(src).
+// func Copy(ctx context.Context, dst Writer, src Reader) (written int64, err error) {
+// 	return copyBuffer(ctx, dst, src, nil)
+// }
+
+// // copyBuffer is the actual implementation of Copy and CopyBuffer.
+// // if buf is nil, one is allocated.
+// func copyBuffer(ctx context.Context, dst Writer, src Reader, buf []byte) (written int64, err error) {
+// 	// If the reader has a WriteTo method, use it to do the copy.
+// 	// Avoids an allocation and a copy.
+// 	if wt, ok := src.(WriterTo); ok {
+// 		return wt.WriteTo(dst)
+// 	}
+// 	// Similarly, if the writer has a ReadFrom method, use it to do the copy.
+// 	if rt, ok := dst.(ReaderFrom); ok {
+// 		return rt.ReadFrom(src)
+// 	}
+// 	if buf == nil {
+// 		size := 32 * 1024
+// 		if l, ok := src.(*LimitedReader); ok && int64(size) > l.N {
+// 			if l.N < 1 {
+// 				size = 1
+// 			} else {
+// 				size = int(l.N)
+// 			}
+// 		}
+// 		buf = make([]byte, size)
+// 	}
+// 	for {
+// 		nr, er := src.Read(ctx, buf)
+// 		if nr > 0 {
+// 			nw, ew := dst.Write(ctx, buf[0:nr])
+// 			if nw < 0 || nr < nw {
+// 				nw = 0
+// 				if ew == nil {
+// 					ew = errInvalidWrite
+// 				}
+// 			}
+// 			written += int64(nw)
+// 			if ew != nil {
+// 				err = ew
+// 				break
+// 			}
+// 			if nr != nw {
+// 				err = io.ErrShortWrite
+// 				break
+// 			}
+// 		}
+// 		if er != nil {
+// 			if er != io.EOF {
+// 				err = er
+// 			}
+// 			break
+// 		}
+// 	}
+// 	return written, err
+// }
diff --git a/pkg/ctxio/filebuffer.go b/pkg/ctxio/filebuffer.go
new file mode 100644
index 0000000..23cb88e
--- /dev/null
+++ b/pkg/ctxio/filebuffer.go
@@ -0,0 +1,180 @@
+package ctxio
+
+import (
+	"bytes"
+	"context"
+	"errors"
+	"io"
+	"os"
+)
+
+// FileBuffer implements interfaces implemented by files.
+// The main purpose of this type is to have an in memory replacement for a
+// file.
+type FileBuffer struct {
+	// buff is the backing buffer
+	buff *bytes.Buffer
+	// index indicates where in the buffer we are at
+	index    int64
+	isClosed bool
+}
+
+var _ FileReader = (*FileBuffer)(nil)
+var _ Writer = (*FileBuffer)(nil)
+
+// NewFileBuffer returns a new populated Buffer
+func NewFileBuffer(b []byte) *FileBuffer {
+	return &FileBuffer{buff: bytes.NewBuffer(b)}
+}
+
+// NewFileBufferFromReader is a convenience method that returns a new populated Buffer
+// whose contents are sourced from a supplied reader by loading it entirely
+// into memory.
+func NewFileBufferFromReader(ctx context.Context, reader Reader) (*FileBuffer, error) {
+	data, err := ReadAll(ctx, reader)
+	if err != nil {
+		return nil, err
+	}
+	return NewFileBuffer(data), nil
+}
+
+// NewFileBufferFromReader is a convenience method that returns a new populated Buffer
+// whose contents are sourced from a supplied reader by loading it entirely
+// into memory.
+func NewFileBufferFromIoReader(reader io.Reader) (*FileBuffer, error) {
+	data, err := io.ReadAll(reader)
+	if err != nil {
+		return nil, err
+	}
+	return NewFileBuffer(data), nil
+}
+
+// Bytes returns the bytes available until the end of the buffer.
+func (f *FileBuffer) Bytes() []byte {
+	if f.isClosed || f.index >= int64(f.buff.Len()) {
+		return []byte{}
+	}
+	return f.buff.Bytes()[f.index:]
+}
+
+// String implements the Stringer interface
+func (f *FileBuffer) String() string {
+	return string(f.buff.Bytes()[f.index:])
+}
+
+// Read implements io.Reader https://golang.org/pkg/io/#Reader
+// Read reads up to len(p) bytes into p. It returns the number of bytes read (0 <= n <= len(p))
+// and any error encountered. Even if Read returns n < len(p), it may use all of p as scratch
+// space during the call. If some data is available but not len(p) bytes, Read conventionally
+// returns what is available instead of waiting for more.
+
+// When Read encounters an error or end-of-file condition after successfully reading n > 0 bytes,
+// it returns the number of bytes read. It may return the (non-nil) error from the same call or
+// return the error (and n == 0) from a subsequent call. An instance of this general case is
+// that a Reader returning a non-zero number of bytes at the end of the input stream may return
+// either err == EOF or err == nil. The next Read should return 0, EOF.
+func (f *FileBuffer) Read(ctx context.Context, b []byte) (n int, err error) {
+	if f.isClosed {
+		return 0, os.ErrClosed
+	}
+	if len(b) == 0 {
+		return 0, nil
+	}
+	if f.index >= int64(f.buff.Len()) {
+		return 0, io.EOF
+	}
+	n, err = bytes.NewBuffer(f.buff.Bytes()[f.index:]).Read(b)
+	f.index += int64(n)
+
+	return n, err
+}
+
+// ReadAt implements io.ReaderAt https://golang.org/pkg/io/#ReaderAt
+// ReadAt reads len(p) bytes into p starting at offset off in the underlying input source.
+// It returns the number of bytes read (0 <= n <= len(p)) and any error encountered.
+//
+// When ReadAt returns n < len(p), it returns a non-nil error explaining why more bytes were not returned.
+// In this respect, ReadAt is stricter than Read.
+//
+// Even if ReadAt returns n < len(p), it may use all of p as scratch space during the call.
+// If some data is available but not len(p) bytes, ReadAt blocks until either all the data is available or an error occurs.
+// In this respect ReadAt is different from Read.
+//
+// If the n = len(p) bytes returned by ReadAt are at the end of the input source,
+// ReadAt may return either err == EOF or err == nil.
+//
+// If ReadAt is reading from an input source with a seek offset,
+// ReadAt should not affect nor be affected by the underlying seek offset.
+// Clients of ReadAt can execute parallel ReadAt calls on the same input source.
+func (f *FileBuffer) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) {
+	if f.isClosed {
+		return 0, os.ErrClosed
+	}
+	if off < 0 {
+		return 0, errors.New("filebuffer.ReadAt: negative offset")
+	}
+	reqLen := len(p)
+	buffLen := int64(f.buff.Len())
+	if off >= buffLen {
+		return 0, io.EOF
+	}
+
+	n = copy(p, f.buff.Bytes()[off:])
+	if n < reqLen {
+		err = io.EOF
+	}
+	return n, err
+}
+
+// Write implements io.Writer https://golang.org/pkg/io/#Writer
+// by appending the passed bytes to the buffer unless the buffer is closed or index negative.
+func (f *FileBuffer) Write(ctx context.Context, p []byte) (n int, err error) {
+	if f.isClosed {
+		return 0, os.ErrClosed
+	}
+	if f.index < 0 {
+		return 0, io.EOF
+	}
+	// we might have rewinded, let's reset the buffer before appending to it
+	idx := int(f.index)
+	buffLen := f.buff.Len()
+	if idx != buffLen && idx <= buffLen {
+		f.buff = bytes.NewBuffer(f.Bytes()[:f.index])
+	}
+	n, err = f.buff.Write(p)
+
+	f.index += int64(n)
+	return n, err
+}
+
+// Seek implements io.Seeker https://golang.org/pkg/io/#Seeker
+func (f *FileBuffer) Seek(offset int64, whence int) (idx int64, err error) {
+	if f.isClosed {
+		return 0, os.ErrClosed
+	}
+
+	var abs int64
+	switch whence {
+	case 0:
+		abs = offset
+	case 1:
+		abs = int64(f.index) + offset
+	case 2:
+		abs = int64(f.buff.Len()) + offset
+	default:
+		return 0, errors.New("filebuffer.Seek: invalid whence")
+	}
+	if abs < 0 {
+		return 0, errors.New("filebuffer.Seek: negative position")
+	}
+	f.index = abs
+	return abs, nil
+}
+
+// Close implements io.Closer https://golang.org/pkg/io/#Closer
+// It closes the buffer, rendering it unusable for I/O. It returns an error, if any.
+func (f *FileBuffer) Close(ctx context.Context) error {
+	f.isClosed = true
+	f.buff = nil
+	return nil
+}
diff --git a/pkg/ctxio/io.go b/pkg/ctxio/io.go
new file mode 100644
index 0000000..fa0c561
--- /dev/null
+++ b/pkg/ctxio/io.go
@@ -0,0 +1,663 @@
+package ctxio
+
+import (
+	"context"
+	"errors"
+	"io"
+	"sync"
+)
+
+// Seek whence values.
+const (
+	SeekStart   = 0 // seek relative to the origin of the file
+	SeekCurrent = 1 // seek relative to the current offset
+	SeekEnd     = 2 // seek relative to the end
+)
+
+// ErrShortWrite means that a write accepted fewer bytes than requested
+// but failed to return an explicit error.
+var ErrShortWrite = io.ErrShortWrite
+
+// errInvalidWrite means that a write returned an impossible count.
+var errInvalidWrite = errors.New("invalid write result")
+
+// ErrShortBuffer means that a read required a longer buffer than was provided.
+var ErrShortBuffer = io.ErrShortBuffer
+
+// EOF is the error returned by Read when no more input is available.
+// (Read must return EOF itself, not an error wrapping EOF,
+// because callers will test for EOF using ==.)
+// Functions should return EOF only to signal a graceful end of input.
+// If the EOF occurs unexpectedly in a structured data stream,
+// the appropriate error is either [ErrUnexpectedEOF] or some other error
+// giving more detail.
+var EOF = io.EOF
+
+// ErrUnexpectedEOF means that EOF was encountered in the
+// middle of reading a fixed-size block or data structure.
+var ErrUnexpectedEOF = io.ErrUnexpectedEOF
+
+// ErrNoProgress is returned by some clients of a [Reader] when
+// many calls to Read have failed to return any data or error,
+// usually the sign of a broken [Reader] implementation.
+var ErrNoProgress = io.ErrNoProgress
+
+// Reader is the interface that wraps the basic Read method.
+//
+// Read reads up to len(p) bytes into p. It returns the number of bytes
+// read (0 <= n <= len(p)) and any error encountered. Even if Read
+// returns n < len(p), it may use all of p as scratch space during the call.
+// If some data is available but not len(p) bytes, Read conventionally
+// returns what is available instead of waiting for more.
+//
+// When Read encounters an error or end-of-file condition after
+// successfully reading n > 0 bytes, it returns the number of
+// bytes read. It may return the (non-nil) error from the same call
+// or return the error (and n == 0) from a subsequent call.
+// An instance of this general case is that a Reader returning
+// a non-zero number of bytes at the end of the input stream may
+// return either err == EOF or err == nil. The next Read should
+// return 0, EOF.
+//
+// Callers should always process the n > 0 bytes returned before
+// considering the error err. Doing so correctly handles I/O errors
+// that happen after reading some bytes and also both of the
+// allowed EOF behaviors.
+//
+// If len(p) == 0, Read should always return n == 0. It may return a
+// non-nil error if some error condition is known, such as EOF.
+//
+// Implementations of Read are discouraged from returning a
+// zero byte count with a nil error, except when len(p) == 0.
+// Callers should treat a return of 0 and nil as indicating that
+// nothing happened; in particular it does not indicate EOF.
+//
+// Implementations must not retain p.
+type Reader interface {
+	Read(ctx context.Context, p []byte) (n int, err error)
+}
+
+// Writer is the interface that wraps the basic Write method.
+//
+// Write writes len(p) bytes from p to the underlying data stream.
+// It returns the number of bytes written from p (0 <= n <= len(p))
+// and any error encountered that caused the write to stop early.
+// Write must return a non-nil error if it returns n < len(p).
+// Write must not modify the slice data, even temporarily.
+//
+// Implementations must not retain p.
+type Writer interface {
+	Write(ctx context.Context, p []byte) (n int, err error)
+}
+
+// Closer is the interface that wraps the basic Close method.
+//
+// The behavior of Close after the first call is undefined.
+// Specific implementations may document their own behavior.
+type Closer interface {
+	Close(ctx context.Context) error
+}
+
+// Seeker is the interface that wraps the basic Seek method.
+//
+// Seek sets the offset for the next Read or Write to offset,
+// interpreted according to whence:
+// [SeekStart] means relative to the start of the file,
+// [SeekCurrent] means relative to the current offset, and
+// [SeekEnd] means relative to the end
+// (for example, offset = -2 specifies the penultimate byte of the file).
+// Seek returns the new offset relative to the start of the
+// file or an error, if any.
+//
+// Seeking to an offset before the start of the file is an error.
+// Seeking to any positive offset may be allowed, but if the new offset exceeds
+// the size of the underlying object the behavior of subsequent I/O operations
+// is implementation-dependent.
+type Seeker interface {
+	Seek(offset int64, whence int) (int64, error)
+}
+
+// ReadWriter is the interface that groups the basic Read and Write methods.
+type ReadWriter interface {
+	Reader
+	Writer
+}
+
+// ReadCloser is the interface that groups the basic Read and Close methods.
+type ReadCloser interface {
+	Reader
+	Closer
+}
+
+// WriteCloser is the interface that groups the basic Write and Close methods.
+type WriteCloser interface {
+	Writer
+	Closer
+}
+
+// ReadWriteCloser is the interface that groups the basic Read, Write and Close methods.
+type ReadWriteCloser interface {
+	Reader
+	Writer
+	Closer
+}
+
+// ReadSeeker is the interface that groups the basic Read and Seek methods.
+type ReadSeeker interface {
+	Reader
+	Seeker
+}
+
+// ReadSeekCloser is the interface that groups the basic Read, Seek and Close
+// methods.
+type ReadSeekCloser interface {
+	Reader
+	Seeker
+	Closer
+}
+
+// WriteSeeker is the interface that groups the basic Write and Seek methods.
+type WriteSeeker interface {
+	Writer
+	Seeker
+}
+
+// ReadWriteSeeker is the interface that groups the basic Read, Write and Seek methods.
+type ReadWriteSeeker interface {
+	Reader
+	Writer
+	Seeker
+}
+
+// ReaderFrom is the interface that wraps the ReadFrom method.
+//
+// ReadFrom reads data from r until EOF or error.
+// The return value n is the number of bytes read.
+// Any error except EOF encountered during the read is also returned.
+//
+// The [Copy] function uses [ReaderFrom] if available.
+type ReaderFrom interface {
+	ReadFrom(ctx context.Context, r Reader) (n int64, err error)
+}
+
+// WriterTo is the interface that wraps the WriteTo method.
+//
+// WriteTo writes data to w until there's no more data to write or
+// when an error occurs. The return value n is the number of bytes
+// written. Any error encountered during the write is also returned.
+//
+// The Copy function uses WriterTo if available.
+type WriterTo interface {
+	WriteTo(ctx context.Context, w Writer) (n int64, err error)
+}
+
+// ReaderAt is the interface that wraps the basic ReadAt method.
+//
+// ReadAt reads len(p) bytes into p starting at offset off in the
+// underlying input source. It returns the number of bytes
+// read (0 <= n <= len(p)) and any error encountered.
+//
+// When ReadAt returns n < len(p), it returns a non-nil error
+// explaining why more bytes were not returned. In this respect,
+// ReadAt is stricter than Read.
+//
+// Even if ReadAt returns n < len(p), it may use all of p as scratch
+// space during the call. If some data is available but not len(p) bytes,
+// ReadAt blocks until either all the data is available or an error occurs.
+// In this respect ReadAt is different from Read.
+//
+// If the n = len(p) bytes returned by ReadAt are at the end of the
+// input source, ReadAt may return either err == EOF or err == nil.
+//
+// If ReadAt is reading from an input source with a seek offset,
+// ReadAt should not affect nor be affected by the underlying
+// seek offset.
+//
+// Clients of ReadAt can execute parallel ReadAt calls on the
+// same input source.
+//
+// Implementations must not retain p.
+type ReaderAt interface {
+	ReadAt(ctx context.Context, p []byte, off int64) (n int, err error)
+}
+
+// WriterAt is the interface that wraps the basic WriteAt method.
+//
+// WriteAt writes len(p) bytes from p to the underlying data stream
+// at offset off. It returns the number of bytes written from p (0 <= n <= len(p))
+// and any error encountered that caused the write to stop early.
+// WriteAt must return a non-nil error if it returns n < len(p).
+//
+// If WriteAt is writing to a destination with a seek offset,
+// WriteAt should not affect nor be affected by the underlying
+// seek offset.
+//
+// Clients of WriteAt can execute parallel WriteAt calls on the same
+// destination if the ranges do not overlap.
+//
+// Implementations must not retain p.
+type WriterAt interface {
+	WriteAt(ctx context.Context, p []byte, off int64) (n int, err error)
+}
+
+// StringWriter is the interface that wraps the WriteString method.
+type StringWriter interface {
+	WriteString(s string) (n int, err error)
+}
+
+// WriteString writes the contents of the string s to w, which accepts a slice of bytes.
+// If w implements [StringWriter], [StringWriter.WriteString] is invoked directly.
+// Otherwise, [Writer.Write] is called exactly once.
+func WriteString(ctx context.Context, w Writer, s string) (n int, err error) {
+	if sw, ok := w.(StringWriter); ok {
+		return sw.WriteString(s)
+	}
+	return w.Write(ctx, []byte(s))
+}
+
+// ReadAtLeast reads from r into buf until it has read at least min bytes.
+// It returns the number of bytes copied and an error if fewer bytes were read.
+// The error is EOF only if no bytes were read.
+// If an EOF happens after reading fewer than min bytes,
+// ReadAtLeast returns [ErrUnexpectedEOF].
+// If min is greater than the length of buf, ReadAtLeast returns [ErrShortBuffer].
+// On return, n >= min if and only if err == nil.
+// If r returns an error having read at least min bytes, the error is dropped.
+func ReadAtLeast(ctx context.Context, r Reader, buf []byte, min int) (n int, err error) {
+	if len(buf) < min {
+		return 0, ErrShortBuffer
+	}
+	for n < min && err == nil {
+		var nn int
+		nn, err = r.Read(ctx, buf[n:])
+		n += nn
+	}
+	if n >= min {
+		err = nil
+	} else if n > 0 && err == EOF {
+		err = ErrUnexpectedEOF
+	}
+	return
+}
+
+// ReadFull reads exactly len(buf) bytes from r into buf.
+// It returns the number of bytes copied and an error if fewer bytes were read.
+// The error is EOF only if no bytes were read.
+// If an EOF happens after reading some but not all the bytes,
+// ReadFull returns [ErrUnexpectedEOF].
+// On return, n == len(buf) if and only if err == nil.
+// If r returns an error having read at least len(buf) bytes, the error is dropped.
+func ReadFull(ctx context.Context, r Reader, buf []byte) (n int, err error) {
+	return ReadAtLeast(ctx, r, buf, len(buf))
+}
+
+// CopyN copies n bytes (or until an error) from src to dst.
+// It returns the number of bytes copied and the earliest
+// error encountered while copying.
+// On return, written == n if and only if err == nil.
+//
+// If dst implements [ReaderFrom], the copy is implemented using it.
+func CopyN(ctx context.Context, dst Writer, src Reader, n int64) (written int64, err error) {
+	written, err = Copy(ctx, dst, LimitReader(src, n))
+	if written == n {
+		return n, nil
+	}
+	if written < n && err == nil {
+		// src stopped early; must have been EOF.
+		err = EOF
+	}
+	return
+}
+
+// Copy copies from src to dst until either EOF is reached
+// on src or an error occurs. It returns the number of bytes
+// copied and the first error encountered while copying, if any.
+//
+// A successful Copy returns err == nil, not err == EOF.
+// Because Copy is defined to read from src until EOF, it does
+// not treat an EOF from Read as an error to be reported.
+//
+// If src implements [WriterTo],
+// the copy is implemented by calling src.WriteTo(dst).
+// Otherwise, if dst implements [ReaderFrom],
+// the copy is implemented by calling dst.ReadFrom(src).
+func Copy(ctx context.Context, dst Writer, src Reader) (written int64, err error) {
+	return copyBuffer(ctx, dst, src, nil)
+}
+
+// CopyBuffer is identical to Copy except that it stages through the
+// provided buffer (if one is required) rather than allocating a
+// temporary one. If buf is nil, one is allocated; otherwise if it has
+// zero length, CopyBuffer panics.
+//
+// If either src implements [WriterTo] or dst implements [ReaderFrom],
+// buf will not be used to perform the copy.
+func CopyBuffer(ctx context.Context, dst Writer, src Reader, buf []byte) (written int64, err error) {
+	if buf != nil && len(buf) == 0 {
+		panic("empty buffer in CopyBuffer")
+	}
+	return copyBuffer(ctx, dst, src, buf)
+}
+
+// copyBuffer is the actual implementation of Copy and CopyBuffer.
+// if buf is nil, one is allocated.
+func copyBuffer(ctx context.Context, dst Writer, src Reader, buf []byte) (written int64, err error) {
+	// If the reader has a WriteTo method, use it to do the copy.
+	// Avoids an allocation and a copy.
+	if wt, ok := src.(WriterTo); ok {
+		return wt.WriteTo(ctx, dst)
+	}
+	// Similarly, if the writer has a ReadFrom method, use it to do the copy.
+	if rt, ok := dst.(ReaderFrom); ok {
+		return rt.ReadFrom(ctx, src)
+	}
+	if buf == nil {
+		size := 32 * 1024
+		if l, ok := src.(*LimitedReader); ok && int64(size) > l.N {
+			if l.N < 1 {
+				size = 1
+			} else {
+				size = int(l.N)
+			}
+		}
+		buf = make([]byte, size)
+	}
+	for {
+		nr, er := src.Read(ctx, buf)
+		if nr > 0 {
+			nw, ew := dst.Write(ctx, buf[0:nr])
+			if nw < 0 || nr < nw {
+				nw = 0
+				if ew == nil {
+					ew = errInvalidWrite
+				}
+			}
+			written += int64(nw)
+			if ew != nil {
+				err = ew
+				break
+			}
+			if nr != nw {
+				err = ErrShortWrite
+				break
+			}
+		}
+		if er != nil {
+			if er != EOF {
+				err = er
+			}
+			break
+		}
+	}
+	return written, err
+}
+
+// LimitReader returns a Reader that reads from r
+// but stops with EOF after n bytes.
+// The underlying implementation is a *LimitedReader.
+func LimitReader(r Reader, n int64) Reader { return &LimitedReader{r, n} }
+
+// A LimitedReader reads from R but limits the amount of
+// data returned to just N bytes. Each call to Read
+// updates N to reflect the new amount remaining.
+// Read returns EOF when N <= 0 or when the underlying R returns EOF.
+type LimitedReader struct {
+	R Reader // underlying reader
+	N int64  // max bytes remaining
+}
+
+func (l *LimitedReader) Read(ctx context.Context, p []byte) (n int, err error) {
+	if l.N <= 0 {
+		return 0, EOF
+	}
+	if int64(len(p)) > l.N {
+		p = p[0:l.N]
+	}
+	n, err = l.R.Read(ctx, p)
+	l.N -= int64(n)
+	return
+}
+
+// NewSectionReader returns a [SectionReader] that reads from r
+// starting at offset off and stops with EOF after n bytes.
+func NewSectionReader(r ReaderAt, off int64, n int64) *SectionReader {
+	var remaining int64
+	const maxint64 = 1<<63 - 1
+	if off <= maxint64-n {
+		remaining = n + off
+	} else {
+		// Overflow, with no way to return error.
+		// Assume we can read up to an offset of 1<<63 - 1.
+		remaining = maxint64
+	}
+	return &SectionReader{r, off, off, remaining, n}
+}
+
+// SectionReader implements Read, Seek, and ReadAt on a section
+// of an underlying [ReaderAt].
+type SectionReader struct {
+	r     ReaderAt // constant after creation
+	base  int64    // constant after creation
+	off   int64
+	limit int64 // constant after creation
+	n     int64 // constant after creation
+}
+
+func (s *SectionReader) Read(ctx context.Context, p []byte) (n int, err error) {
+	if s.off >= s.limit {
+		return 0, EOF
+	}
+	if max := s.limit - s.off; int64(len(p)) > max {
+		p = p[0:max]
+	}
+	n, err = s.r.ReadAt(ctx, p, s.off)
+	s.off += int64(n)
+	return
+}
+
+var errWhence = errors.New("Seek: invalid whence")
+var errOffset = errors.New("Seek: invalid offset")
+
+func (s *SectionReader) Seek(offset int64, whence int) (int64, error) {
+	switch whence {
+	default:
+		return 0, errWhence
+	case SeekStart:
+		offset += s.base
+	case SeekCurrent:
+		offset += s.off
+	case SeekEnd:
+		offset += s.limit
+	}
+	if offset < s.base {
+		return 0, errOffset
+	}
+	s.off = offset
+	return offset - s.base, nil
+}
+
+func (s *SectionReader) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) {
+	if off < 0 || off >= s.Size() {
+		return 0, EOF
+	}
+	off += s.base
+	if max := s.limit - off; int64(len(p)) > max {
+		p = p[0:max]
+		n, err = s.r.ReadAt(ctx, p, off)
+		if err == nil {
+			err = EOF
+		}
+		return n, err
+	}
+	return s.r.ReadAt(ctx, p, off)
+}
+
+// Size returns the size of the section in bytes.
+func (s *SectionReader) Size() int64 { return s.limit - s.base }
+
+// Outer returns the underlying [ReaderAt] and offsets for the section.
+//
+// The returned values are the same that were passed to [NewSectionReader]
+// when the [SectionReader] was created.
+func (s *SectionReader) Outer() (r ReaderAt, off int64, n int64) {
+	return s.r, s.base, s.n
+}
+
+// An OffsetWriter maps writes at offset base to offset base+off in the underlying writer.
+type OffsetWriter struct {
+	w    WriterAt
+	base int64 // the original offset
+	off  int64 // the current offset
+}
+
+// NewOffsetWriter returns an [OffsetWriter] that writes to w
+// starting at offset off.
+func NewOffsetWriter(w WriterAt, off int64) *OffsetWriter {
+	return &OffsetWriter{w, off, off}
+}
+
+func (o *OffsetWriter) Write(ctx context.Context, p []byte) (n int, err error) {
+	n, err = o.w.WriteAt(ctx, p, o.off)
+	o.off += int64(n)
+	return
+}
+
+func (o *OffsetWriter) WriteAt(ctx context.Context, p []byte, off int64) (n int, err error) {
+	if off < 0 {
+		return 0, errOffset
+	}
+
+	off += o.base
+	return o.w.WriteAt(ctx, p, off)
+}
+
+func (o *OffsetWriter) Seek(offset int64, whence int) (int64, error) {
+	switch whence {
+	default:
+		return 0, errWhence
+	case SeekStart:
+		offset += o.base
+	case SeekCurrent:
+		offset += o.off
+	}
+	if offset < o.base {
+		return 0, errOffset
+	}
+	o.off = offset
+	return offset - o.base, nil
+}
+
+// TeeReader returns a [Reader] that writes to w what it reads from r.
+// All reads from r performed through it are matched with
+// corresponding writes to w. There is no internal buffering -
+// the write must complete before the read completes.
+// Any error encountered while writing is reported as a read error.
+func TeeReader(r Reader, w Writer) Reader {
+	return &teeReader{r, w}
+}
+
+type teeReader struct {
+	r Reader
+	w Writer
+}
+
+func (t *teeReader) Read(ctx context.Context, p []byte) (n int, err error) {
+	n, err = t.r.Read(ctx, p)
+	if n > 0 {
+		if n, err := t.w.Write(ctx, p[:n]); err != nil {
+			return n, err
+		}
+	}
+	return
+}
+
+// Discard is a [Writer] on which all Write calls succeed
+// without doing anything.
+var Discard Writer = discard{}
+
+type discard struct{}
+
+// discard implements ReaderFrom as an optimization so Copy to
+// io.Discard can avoid doing unnecessary work.
+var _ ReaderFrom = discard{}
+
+func (discard) Write(ctx context.Context, p []byte) (int, error) {
+	return len(p), nil
+}
+
+func (discard) WriteString(ctx context.Context, s string) (int, error) {
+	return len(s), nil
+}
+
+var blackHolePool = sync.Pool{
+	New: func() any {
+		b := make([]byte, 8192)
+		return &b
+	},
+}
+
+func (discard) ReadFrom(ctx context.Context, r Reader) (n int64, err error) {
+	bufp := blackHolePool.Get().(*[]byte)
+	readSize := 0
+	for {
+		readSize, err = r.Read(ctx, *bufp)
+		n += int64(readSize)
+		if err != nil {
+			blackHolePool.Put(bufp)
+			if err == EOF {
+				return n, nil
+			}
+			return
+		}
+	}
+}
+
+// NopCloser returns a [ReadCloser] with a no-op Close method wrapping
+// the provided [Reader] r.
+// If r implements [WriterTo], the returned [ReadCloser] will implement [WriterTo]
+// by forwarding calls to r.
+func NopCloser(r Reader) ReadCloser {
+	if _, ok := r.(WriterTo); ok {
+		return nopCloserWriterTo{r}
+	}
+	return nopCloser{r}
+}
+
+type nopCloser struct {
+	Reader
+}
+
+func (nopCloser) Close(ctx context.Context) error { return nil }
+
+type nopCloserWriterTo struct {
+	Reader
+}
+
+func (nopCloserWriterTo) Close(ctx context.Context) error { return nil }
+
+func (c nopCloserWriterTo) WriteTo(ctx context.Context, w Writer) (n int64, err error) {
+	return c.Reader.(WriterTo).WriteTo(ctx, w)
+}
+
+// ReadAll reads from r until an error or EOF and returns the data it read.
+// A successful call returns err == nil, not err == EOF. Because ReadAll is
+// defined to read from src until EOF, it does not treat an EOF from Read
+// as an error to be reported.
+func ReadAll(ctx context.Context, r Reader) ([]byte, error) {
+	b := make([]byte, 0, 512)
+	for {
+		n, err := r.Read(ctx, b[len(b):cap(b)])
+		b = b[:len(b)+n]
+		if err != nil {
+			if err == EOF {
+				err = nil
+			}
+			return b, err
+		}
+
+		if len(b) == cap(b) {
+			// Add more capacity (let append pick how much).
+			b = append(b, 0)[:len(b)]
+		}
+	}
+}
diff --git a/pkg/ctxio/reader.go b/pkg/ctxio/reader.go
index 2ba9f7d..8d2e4c7 100644
--- a/pkg/ctxio/reader.go
+++ b/pkg/ctxio/reader.go
@@ -5,28 +5,22 @@ import (
 	"io"
 )
 
-type ReaderAtCloser interface {
+type FileReader interface {
+	Reader
 	ReaderAt
 	Closer
 }
 
-type ReaderAt interface {
-	ReadAt(ctx context.Context, p []byte, off int64) (n int, err error)
-}
-
-type Reader interface {
-	Read(ctx context.Context, p []byte) (n int, err error)
-}
-
-type Closer interface {
-	Close(ctx context.Context) error
-}
 type contextReader struct {
 	ctx context.Context
 	r   Reader
 }
 
 func (r *contextReader) Read(p []byte) (n int, err error) {
+	if r.ctx.Err() != nil {
+		return 0, r.ctx.Err()
+	}
+
 	return r.r.Read(r.ctx, p)
 }
 
@@ -40,9 +34,31 @@ type contextReaderAt struct {
 }
 
 func (c *contextReaderAt) ReadAt(p []byte, off int64) (n int, err error) {
+	if c.ctx.Err() != nil {
+		return 0, c.ctx.Err()
+	}
+
 	return c.r.ReadAt(c.ctx, p, off)
 }
 
 func IoReader(ctx context.Context, r Reader) io.Reader {
 	return &contextReader{ctx: ctx, r: r}
 }
+
+func WrapIoReader(r io.Reader) Reader {
+	return &wrapReader{r: r}
+}
+
+type wrapReader struct {
+	r io.Reader
+}
+
+var _ Reader = (*wrapReader)(nil)
+
+// Read implements Reader.
+func (c *wrapReader) Read(ctx context.Context, p []byte) (n int, err error) {
+	if ctx.Err() != nil {
+		return 0, ctx.Err()
+	}
+	return c.r.Read(p)
+}
diff --git a/pkg/ctxio/seeker.go b/pkg/ctxio/seeker.go
index 5284ecc..a482477 100644
--- a/pkg/ctxio/seeker.go
+++ b/pkg/ctxio/seeker.go
@@ -59,10 +59,10 @@ type ioSeekerCloserWrapper struct {
 	pos  int64
 	size int64
 
-	r ReaderAtCloser
+	r FileReader
 }
 
-func IoReadSeekCloserWrapper(ctx context.Context, r ReaderAtCloser, size int64) io.ReadSeekCloser {
+func IoReadSeekCloserWrapper(ctx context.Context, r FileReader, size int64) io.ReadSeekCloser {
 	return &ioSeekerCloserWrapper{
 		ctx:  ctx,
 		r:    r,
diff --git a/pkg/ctxio/teereader.go b/pkg/ctxio/teereader.go
new file mode 100644
index 0000000..999a670
--- /dev/null
+++ b/pkg/ctxio/teereader.go
@@ -0,0 +1,20 @@
+package ctxio
+
+// func TeeReader(r Reader, w Writer) Reader {
+// 	return &teeReader{r, w}
+// }
+
+// type teeReader struct {
+// 	r Reader
+// 	w Writer
+// }
+
+// func (t *teeReader) Read(ctx context.Context, p []byte) (n int, err error) {
+// 	n, err = t.r.Read(ctx, p)
+// 	if n > 0 {
+// 		if n, err := t.w.Write(ctx, p[:n]); err != nil {
+// 			return n, err
+// 		}
+// 	}
+// 	return
+// }
diff --git a/pkg/go-nfs/.github/dependabot.yml b/pkg/go-nfs/.github/dependabot.yml
new file mode 100644
index 0000000..12389a7
--- /dev/null
+++ b/pkg/go-nfs/.github/dependabot.yml
@@ -0,0 +1,11 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+  - package-ecosystem: "gomod"
+    directory: "/" # Location of package manifests
+    schedule:
+      interval: "daily"
diff --git a/pkg/go-nfs/.github/workflows/codeql-analysis.yml b/pkg/go-nfs/.github/workflows/codeql-analysis.yml
new file mode 100644
index 0000000..ca22cc4
--- /dev/null
+++ b/pkg/go-nfs/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,51 @@
+name: "Code scanning - action"
+
+on:
+  push:
+  pull_request:
+  schedule:
+    - cron: '0 18 * * 3'
+
+jobs:
+  CodeQL-Build:
+
+    runs-on: ubuntu-latest
+
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v2
+      with:
+        # We must fetch at least the immediate parents so that if this is
+        # a pull request then we can checkout the head.
+        fetch-depth: 2
+
+    # If this run was triggered by a pull request event, then checkout
+    # the head of the pull request instead of the merge commit.
+    - run: git checkout HEAD^2
+      if: ${{ github.event_name == 'pull_request' }}
+      
+    # Initializes the CodeQL tools for scanning.
+    - name: Initialize CodeQL
+      uses: github/codeql-action/init@v1
+      # Override language selection by uncommenting this and choosing your languages
+      # with:
+      #   languages: go, javascript, csharp, python, cpp, java
+
+    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
+    # If this step fails, then you should remove it and run the build manually (see below)
+    - name: Autobuild
+      uses: github/codeql-action/autobuild@v1
+
+    # â„šī¸ Command-line programs to run using the OS shell.
+    # 📚 https://git.io/JvXDl
+
+    # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines
+    #    and modify them (or add more) to build your code if your project
+    #    uses a compiled language
+
+    #- run: |
+    #   make bootstrap
+    #   make release
+
+    - name: Perform CodeQL Analysis
+      uses: github/codeql-action/analyze@v1
diff --git a/pkg/go-nfs/.github/workflows/go.yml b/pkg/go-nfs/.github/workflows/go.yml
new file mode 100644
index 0000000..df1e4fe
--- /dev/null
+++ b/pkg/go-nfs/.github/workflows/go.yml
@@ -0,0 +1,36 @@
+name: Go
+
+on:
+  push:
+    branches: [ master ]
+  pull_request:
+    branches: [ master ]
+
+permissions:
+  contents: read
+
+jobs:
+  build:
+    name: Build
+    runs-on: ubuntu-latest
+    steps:
+    - name: Set up Go 1.x
+      uses: actions/setup-go@v3
+      with:
+        go-version: ^1.19
+      id: go
+
+    - name: Check out code into the Go module directory
+      uses: actions/checkout@v3
+
+    - name: Get dependencies
+      run: go get -v -t -d ./...
+
+    - name: Build
+      run: go build -v ./...
+
+    - name: golangci-lint
+      uses: golangci/golangci-lint-action@v3
+
+    - name: Test
+      run: go test -v .
diff --git a/pkg/go-nfs/CONTRIBUTING.md b/pkg/go-nfs/CONTRIBUTING.md
new file mode 100644
index 0000000..f1f3f11
--- /dev/null
+++ b/pkg/go-nfs/CONTRIBUTING.md
@@ -0,0 +1,11 @@
+# Contributing Guidelines
+
+We appreciate your interest in improving go-nfs!
+
+## Looking for ways to contribute?
+
+There are several ways you can contribute:
+- Start contributing immediately via the [opened](https://github.com/willscott/go-nfs/issues) issues on GitHub.
+  Defined issues provide an excellent starting point.
+- Reporting issues, bugs, mistakes, or inconsistencies.
+  As many open source projects, we are short-staffed, we thus kindly ask you to be open to contribute a fix for discovered issues.
diff --git a/pkg/go-nfs/LICENSE b/pkg/go-nfs/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/pkg/go-nfs/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
diff --git a/pkg/go-nfs/README.md b/pkg/go-nfs/README.md
new file mode 100644
index 0000000..a92f706
--- /dev/null
+++ b/pkg/go-nfs/README.md
@@ -0,0 +1,96 @@
+Golang Network File Server
+===
+
+NFSv3 protocol implementation in pure Golang.
+
+Current Status:
+* Minimally tested
+* Mounts, read-only and read-write support
+
+Usage
+===
+
+The most interesting demo is currently in `example/osview`. 
+
+Start the server
+`go run ./example/osview .`.
+
+The local folder at `.` will be the initial view in the mount. mutations to metadata or contents
+will be stored purely in memory and not written back to the OS. When run, this
+demo will print the port it is listening on.
+
+The mount can be accessed using a command similar to 
+`mount -o port=<n>,mountport=<n> -t nfs localhost:/mount <mountpoint>` (For Mac users)
+
+or
+
+`mount -o port=<n>,mountport=<n>,nfsvers=3,noacl,tcp -t nfs localhost:/mount <mountpoint>` (For Linux users)
+
+API
+===
+
+The NFS server runs on a `net.Listener` to export a file system to NFS clients.
+Usage is structured similarly to many other golang network servers.
+
+```golang
+package main
+
+import (
+	"fmt"
+	"log"
+	"net"
+
+	"github.com/go-git/go-billy/v5/memfs"
+	nfs "github.com/willscott/go-nfs"
+	nfshelper "github.com/willscott/go-nfs/helpers"
+)
+
+func main() {
+	listener, err := net.Listen("tcp", ":0")
+	panicOnErr(err, "starting TCP listener")
+	fmt.Printf("Server running at %s\n", listener.Addr())
+	mem := memfs.New()
+	f, err := mem.Create("hello.txt")
+	panicOnErr(err, "creating file")
+	_, err = f.Write([]byte("hello world"))
+	panicOnErr(err, "writing data")
+	f.Close()
+	handler := nfshelper.NewNullAuthHandler(mem)
+	cacheHelper := nfshelper.NewCachingHandler(handler, 1)
+	panicOnErr(nfs.Serve(listener, cacheHelper), "serving nfs")
+}
+
+func panicOnErr(err error, desc ...interface{}) {
+	if err == nil {
+		return
+	}
+	log.Println(desc...)
+	log.Panicln(err)
+}
+```
+
+Notes
+---
+
+* Ports are typically determined through portmap. The need for running portmap 
+(which is the only part that needs a privileged listening port) can be avoided
+through specific mount options. e.g. 
+`mount -o port=n,mountport=n -t nfs host:/mount /localmount`
+
+* This server currently uses [billy](https://github.com/go-git/go-billy/) to
+provide a file system abstraction layer. There are some edges of the NFS protocol
+which do not translate to this abstraction.
+  * NFS expects access to an `inode` or equivalent unique identifier to reference
+  files in a file system. These are considered opaque identifiers here, which
+  means they will not work as expected in cases of hard linking.
+  * The billy abstraction layer does not extend to exposing `uid` and `gid`
+  ownership of files. If ownership is important to your file system, you
+  will need to ensure that the `os.FileInfo` meets additional constraints.
+  In particular, the `Sys()` escape hatch is queried by this library, and
+  if your file system populates a [`syscall.Stat_t`](https://golang.org/pkg/syscall/#Stat_t)
+  concrete struct, the ownership specified in that object will be used.
+
+* Relevant RFCS:
+[5531 - RPC protocol](https://tools.ietf.org/html/rfc5531),
+[1813 - NFSv3](https://tools.ietf.org/html/rfc1813),
+[1094 - NFS](https://tools.ietf.org/html/rfc1094)
diff --git a/pkg/go-nfs/SECURITY.md b/pkg/go-nfs/SECURITY.md
new file mode 100644
index 0000000..5f079c1
--- /dev/null
+++ b/pkg/go-nfs/SECURITY.md
@@ -0,0 +1,11 @@
+# Security Policy
+
+## Supported Versions
+
+The latest release reflects the current best recommendation / supported version at this time.
+
+## Reporting a Vulnerability
+
+Please email Will (the git commit author) if you need to report issues privately.
+I will endeavor to respond within a day, but if I am offline, responses may be delayed longer than that.
+If you need a stronger SLA to have confidence in using this code, feel free to reach out.
diff --git a/pkg/go-nfs/capability_check.go b/pkg/go-nfs/capability_check.go
new file mode 100644
index 0000000..5ad6596
--- /dev/null
+++ b/pkg/go-nfs/capability_check.go
@@ -0,0 +1,9 @@
+package nfs
+
+import (
+	billy "github.com/go-git/go-billy/v5"
+)
+
+func CapabilityCheck(fs Filesystem, cap billy.Capability) bool {
+	return true
+}
diff --git a/pkg/go-nfs/conn.go b/pkg/go-nfs/conn.go
new file mode 100644
index 0000000..ec9cf76
--- /dev/null
+++ b/pkg/go-nfs/conn.go
@@ -0,0 +1,335 @@
+package nfs
+
+import (
+	"bufio"
+	"bytes"
+	"context"
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"io"
+	"net"
+
+	xdr2 "github.com/rasky/go-xdr/xdr2"
+	"github.com/willscott/go-nfs-client/nfs/rpc"
+	"github.com/willscott/go-nfs-client/nfs/xdr"
+	"go.opentelemetry.io/otel"
+)
+
+var (
+	// ErrInputInvalid is returned when input cannot be parsed
+	ErrInputInvalid = errors.New("invalid input")
+	// ErrAlreadySent is returned when writing a header/status multiple times
+	ErrAlreadySent = errors.New("response already started")
+)
+
+// ResponseCode is a combination of accept_stat and reject_stat.
+type ResponseCode uint32
+
+// ResponseCode Codes
+const (
+	ResponseCodeSuccess ResponseCode = iota
+	ResponseCodeProgUnavailable
+	ResponseCodeProcUnavailable
+	ResponseCodeGarbageArgs
+	ResponseCodeSystemErr
+	ResponseCodeRPCMismatch
+	ResponseCodeAuthError
+)
+
+type conn struct {
+	*Server
+	writeSerializer chan []byte
+	net.Conn
+}
+
+var tracer = otel.Tracer("git.kmsign.ru/royalcat/tstor/pkg/go-nfs")
+
+func (c *conn) serve() {
+	ctx := context.Background() // TODO implement correct timeout on serve side
+
+	c.writeSerializer = make(chan []byte, 1)
+	go c.serializeWrites(ctx)
+
+	bio := bufio.NewReader(c.Conn)
+	for {
+		w, err := c.readRequestHeader(ctx, bio)
+		if err != nil {
+			if err == io.EOF {
+				// Clean close.
+				c.Close()
+				return
+			}
+			return
+		}
+		Log.Tracef("request: %v", w.req)
+		err = c.handle(ctx, w)
+		respErr := w.finish(ctx)
+		if err != nil {
+			Log.Errorf("error handling req: %v", err)
+			// failure to handle at a level needing to close the connection.
+			c.Close()
+			return
+		}
+		if respErr != nil {
+			Log.Errorf("error sending response: %v", respErr)
+			c.Close()
+			return
+		}
+	}
+}
+
+func (c *conn) serializeWrites(ctx context.Context) {
+	// todo: maybe don't need the extra buffer
+	writer := bufio.NewWriter(c.Conn)
+	var fragmentBuf [4]byte
+	var fragmentInt uint32
+	for {
+		select {
+		case <-ctx.Done():
+			return
+		case msg, ok := <-c.writeSerializer:
+			if !ok {
+				return
+			}
+			// prepend the fragmentation header
+			fragmentInt = uint32(len(msg))
+			fragmentInt |= (1 << 31)
+			binary.BigEndian.PutUint32(fragmentBuf[:], fragmentInt)
+			n, err := writer.Write(fragmentBuf[:])
+			if n < 4 || err != nil {
+				return
+			}
+			n, err = writer.Write(msg)
+			if err != nil {
+				return
+			}
+			if n < len(msg) {
+				panic("todo: ensure writes complete fully.")
+			}
+			if err = writer.Flush(); err != nil {
+				return
+			}
+		}
+	}
+}
+
+// Handle a request. errors from this method indicate a failure to read or
+// write on the network stream, and trigger a disconnection of the connection.
+func (c *conn) handle(ctx context.Context, w *response) error {
+	ctx, span := tracer.Start(ctx, fmt.Sprintf("nfs.handle.%s", NFSProcedure(w.req.Header.Proc).String()))
+	defer span.End()
+
+	handler := c.Server.handlerFor(w.req.Header.Prog, w.req.Header.Proc)
+	if handler == nil {
+		Log.Errorf("No handler for %d.%d", w.req.Header.Prog, w.req.Header.Proc)
+		if err := w.drain(ctx); err != nil {
+			return err
+		}
+		return c.err(ctx, w, &ResponseCodeProcUnavailableError{})
+	}
+	appError := handler(ctx, w, c.Server.Handler)
+	if drainErr := w.drain(ctx); drainErr != nil {
+		return drainErr
+	}
+	if appError != nil && !w.responded {
+		Log.Errorf("call to %+v failed: %v", handler, appError)
+		if err := c.err(ctx, w, appError); err != nil {
+			return err
+		}
+	}
+	if !w.responded {
+		Log.Errorf("Handler did not indicate response status via writing or erroring")
+		if err := c.err(ctx, w, &ResponseCodeSystemError{}); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (c *conn) err(ctx context.Context, w *response, err error) error {
+	select {
+	case <-ctx.Done():
+		return nil
+	default:
+	}
+
+	if w.err == nil {
+		w.err = err
+	}
+
+	if w.responded {
+		return nil
+	}
+
+	rpcErr := w.errorFmt(err)
+	if writeErr := w.writeHeader(rpcErr.Code()); writeErr != nil {
+		return writeErr
+	}
+
+	body, _ := rpcErr.MarshalBinary()
+	return w.Write(body)
+}
+
+type request struct {
+	xid uint32
+	rpc.Header
+	Body io.Reader
+}
+
+func (r *request) String() string {
+	if r.Header.Prog == nfsServiceID {
+		return fmt.Sprintf("RPC #%d (nfs.%s)", r.xid, NFSProcedure(r.Header.Proc))
+	} else if r.Header.Prog == mountServiceID {
+		return fmt.Sprintf("RPC #%d (mount.%s)", r.xid, MountProcedure(r.Header.Proc))
+	}
+	return fmt.Sprintf("RPC #%d (%d.%d)", r.xid, r.Header.Prog, r.Header.Proc)
+}
+
+type response struct {
+	*conn
+	writer    *bytes.Buffer
+	responded bool
+	err       error
+	errorFmt  func(error) RPCError
+	req       *request
+}
+
+func (w *response) writeXdrHeader() error {
+	err := xdr.Write(w.writer, &w.req.xid)
+	if err != nil {
+		return err
+	}
+	respType := uint32(1)
+	err = xdr.Write(w.writer, &respType)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (w *response) writeHeader(code ResponseCode) error {
+	if w.responded {
+		return ErrAlreadySent
+	}
+	w.responded = true
+	if err := w.writeXdrHeader(); err != nil {
+		return err
+	}
+
+	status := rpc.MsgAccepted
+	if code == ResponseCodeAuthError || code == ResponseCodeRPCMismatch {
+		status = rpc.MsgDenied
+	}
+
+	err := xdr.Write(w.writer, &status)
+	if err != nil {
+		return err
+	}
+
+	if status == rpc.MsgAccepted {
+		// Write opaque_auth header.
+		err = xdr.Write(w.writer, &rpc.AuthNull)
+		if err != nil {
+			return err
+		}
+	}
+
+	return xdr.Write(w.writer, &code)
+}
+
+// Write a response to an xdr message
+func (w *response) Write(dat []byte) error {
+	if !w.responded {
+		if err := w.writeHeader(ResponseCodeSuccess); err != nil {
+			return err
+		}
+	}
+
+	acc := 0
+	for acc < len(dat) {
+		n, err := w.writer.Write(dat[acc:])
+		if err != nil {
+			return err
+		}
+		acc += n
+	}
+	return nil
+}
+
+// drain reads the rest of the request frame if not consumed by the handler.
+func (w *response) drain(ctx context.Context) error {
+	if reader, ok := w.req.Body.(*io.LimitedReader); ok {
+		if reader.N == 0 {
+			return nil
+		}
+		// todo: wrap body in a context reader.
+		_, err := io.CopyN(io.Discard, w.req.Body, reader.N)
+		if err == nil || err == io.EOF {
+			return nil
+		}
+		return err
+	}
+	return io.ErrUnexpectedEOF
+}
+
+func (w *response) finish(ctx context.Context) error {
+	select {
+	case w.conn.writeSerializer <- w.writer.Bytes():
+		return nil
+	case <-ctx.Done():
+		return ctx.Err()
+	}
+}
+
+func (c *conn) readRequestHeader(ctx context.Context, reader *bufio.Reader) (w *response, err error) {
+	fragment, err := xdr.ReadUint32(reader)
+	if err != nil {
+		if xdrErr, ok := err.(*xdr2.UnmarshalError); ok {
+			if xdrErr.Err == io.EOF {
+				return nil, io.EOF
+			}
+		}
+		return nil, err
+	}
+	if fragment&(1<<31) == 0 {
+		Log.Warnf("Warning: haven't implemented fragment reconstruction.\n")
+		return nil, ErrInputInvalid
+	}
+	reqLen := fragment - uint32(1<<31)
+	if reqLen < 40 {
+		return nil, ErrInputInvalid
+	}
+
+	r := io.LimitedReader{R: reader, N: int64(reqLen)}
+
+	xid, err := xdr.ReadUint32(&r)
+	if err != nil {
+		return nil, err
+	}
+	reqType, err := xdr.ReadUint32(&r)
+	if err != nil {
+		return nil, err
+	}
+	if reqType != 0 { // 0 = request, 1 = response
+		return nil, ErrInputInvalid
+	}
+
+	req := request{
+		xid,
+		rpc.Header{},
+		&r,
+	}
+	if err = xdr.Read(&r, &req.Header); err != nil {
+		return nil, err
+	}
+
+	w = &response{
+		conn:     c,
+		req:      &req,
+		errorFmt: basicErrorFormatter,
+		// TODO: use a pool for these.
+		writer: bytes.NewBuffer([]byte{}),
+	}
+	return w, nil
+}
diff --git a/pkg/go-nfs/errors.go b/pkg/go-nfs/errors.go
new file mode 100644
index 0000000..af08be6
--- /dev/null
+++ b/pkg/go-nfs/errors.go
@@ -0,0 +1,230 @@
+package nfs
+
+import (
+	"encoding"
+	"encoding/binary"
+	"errors"
+	"fmt"
+)
+
+// RPCError provides the error interface for errors thrown by
+// procedures to be transmitted over the XDR RPC channel
+type RPCError interface {
+	// An RPCError is an `error` with this method
+	Error() string
+	// Code is the RPC Response code to send
+	Code() ResponseCode
+	// BinaryMarshaler is the on-wire representation of this error
+	encoding.BinaryMarshaler
+}
+
+// AuthStat is an enumeration of why authentication ahs failed
+type AuthStat uint32
+
+// AuthStat Codes
+const (
+	AuthStatOK AuthStat = iota
+	AuthStatBadCred
+	AuthStatRejectedCred
+	AuthStatBadVerifier
+	AuthStatRejectedVerfier
+	AuthStatTooWeak
+	AuthStatInvalidResponse
+	AuthStatFailed
+	AuthStatKerbGeneric
+	AuthStatTimeExpire
+	AuthStatTktFile
+	AuthStatDecode
+	AuthStatNetAddr
+	AuthStatRPCGSSCredProblem
+	AuthStatRPCGSSCTXProblem
+)
+
+// AuthError is an RPCError
+type AuthError struct {
+	AuthStat
+}
+
+// Code for AuthErrors is ResponseCodeAuthError
+func (a *AuthError) Code() ResponseCode {
+	return ResponseCodeAuthError
+}
+
+// Error is a textual representaiton of the auth error. From the RFC
+func (a *AuthError) Error() string {
+	switch a.AuthStat {
+	case AuthStatOK:
+		return "Auth Status: OK"
+	case AuthStatBadCred:
+		return "Auth Status: bad credential"
+	case AuthStatRejectedCred:
+		return "Auth Status: client must begin new session"
+	case AuthStatBadVerifier:
+		return "Auth Status: bad verifier"
+	case AuthStatRejectedVerfier:
+		return "Auth Status: verifier expired or replayed"
+	case AuthStatTooWeak:
+		return "Auth Status: rejected for security reasons"
+	case AuthStatInvalidResponse:
+		return "Auth Status: bogus response verifier"
+	case AuthStatFailed:
+		return "Auth Status: reason unknown"
+	case AuthStatKerbGeneric:
+		return "Auth Status: kerberos generic error"
+	case AuthStatTimeExpire:
+		return "Auth Status: time of credential expired"
+	case AuthStatTktFile:
+		return "Auth Status: problem with ticket file"
+	case AuthStatDecode:
+		return "Auth Status: can't decode authenticator"
+	case AuthStatNetAddr:
+		return "Auth Status: wrong net address in ticket"
+	case AuthStatRPCGSSCredProblem:
+		return "Auth Status: no credentials for user"
+	case AuthStatRPCGSSCTXProblem:
+		return "Auth Status: problem with context"
+	}
+	return "Auth Status: Unknown"
+}
+
+// MarshalBinary sends the specific auth status
+func (a *AuthError) MarshalBinary() (data []byte, err error) {
+	var resp [4]byte
+	binary.LittleEndian.PutUint32(resp[:], uint32(a.AuthStat))
+	return resp[:], nil
+}
+
+// RPCMismatchError is an RPCError
+type RPCMismatchError struct {
+	Low  uint32
+	High uint32
+}
+
+// Code for RPCMismatchError is ResponseCodeRPCMismatch
+func (r *RPCMismatchError) Code() ResponseCode {
+	return ResponseCodeRPCMismatch
+}
+
+func (r *RPCMismatchError) Error() string {
+	return fmt.Sprintf("RPC Mismatch: Expected version between %d and %d.", r.Low, r.High)
+}
+
+// MarshalBinary sends the specific rpc mismatch range
+func (r *RPCMismatchError) MarshalBinary() (data []byte, err error) {
+	var resp [8]byte
+	binary.LittleEndian.PutUint32(resp[0:4], uint32(r.Low))
+	binary.LittleEndian.PutUint32(resp[4:8], uint32(r.High))
+	return resp[:], nil
+}
+
+// ResponseCodeProcUnavailableError is an RPCError
+type ResponseCodeProcUnavailableError struct {
+}
+
+// Code for ResponseCodeProcUnavailableError
+func (r *ResponseCodeProcUnavailableError) Code() ResponseCode {
+	return ResponseCodeProcUnavailable
+}
+
+func (r *ResponseCodeProcUnavailableError) Error() string {
+	return "The requested procedure is unexported"
+}
+
+// MarshalBinary - this error has no associated body
+func (r *ResponseCodeProcUnavailableError) MarshalBinary() (data []byte, err error) {
+	return []byte{}, nil
+}
+
+// ResponseCodeSystemError is an RPCError
+type ResponseCodeSystemError struct {
+}
+
+// Code for ResponseCodeSystemError
+func (r *ResponseCodeSystemError) Code() ResponseCode {
+	return ResponseCodeSystemErr
+}
+
+func (r *ResponseCodeSystemError) Error() string {
+	return "memory allocation failure"
+}
+
+// MarshalBinary - this error has no associated body
+func (r *ResponseCodeSystemError) MarshalBinary() (data []byte, err error) {
+	return []byte{}, nil
+}
+
+// basicErrorFormatter is the default error handler for response errors.
+// if the error is already formatted, it is directly written. Otherwise,
+// ResponseCodeSystemError is sent to the client.
+func basicErrorFormatter(err error) RPCError {
+	var rpcErr RPCError
+	if errors.As(err, &rpcErr) {
+		return rpcErr
+	}
+	return &ResponseCodeSystemError{}
+}
+
+// NFSStatusError represents an error at the NFS level.
+type NFSStatusError struct {
+	NFSStatus
+	WrappedErr error
+}
+
+// Error is The wrapped error
+func (s *NFSStatusError) Error() string {
+	message := s.NFSStatus.String()
+	if s.WrappedErr != nil {
+		message = fmt.Sprintf("%s: %v", message, s.WrappedErr)
+	}
+	return message
+}
+
+// Code for NFS issues are successful RPC responses
+func (s *NFSStatusError) Code() ResponseCode {
+	return ResponseCodeSuccess
+}
+
+// MarshalBinary - The binary form of the code.
+func (s *NFSStatusError) MarshalBinary() (data []byte, err error) {
+	var resp [4]byte
+	binary.BigEndian.PutUint32(resp[0:4], uint32(s.NFSStatus))
+	return resp[:], nil
+}
+
+// Unwrap unpacks wrapped errors
+func (s *NFSStatusError) Unwrap() error {
+	return s.WrappedErr
+}
+
+// StatusErrorWithBody is an NFS error with a payload.
+type StatusErrorWithBody struct {
+	NFSStatusError
+	Body []byte
+}
+
+// MarshalBinary provides the wire format of the error response
+func (s *StatusErrorWithBody) MarshalBinary() (data []byte, err error) {
+	head, err := s.NFSStatusError.MarshalBinary()
+	return append(head, s.Body...), err
+}
+
+// errFormatterWithBody appends a provided body to errors
+func errFormatterWithBody(body []byte) func(err error) RPCError {
+	return func(err error) RPCError {
+		if nerr, ok := err.(*NFSStatusError); ok {
+			return &StatusErrorWithBody{*nerr, body[:]}
+		}
+		var rErr RPCError
+		if errors.As(err, &rErr) {
+			return rErr
+		}
+		return &ResponseCodeSystemError{}
+	}
+}
+
+var (
+	opAttrErrorBody       = [4]byte{}
+	opAttrErrorFormatter  = errFormatterWithBody(opAttrErrorBody[:])
+	wccDataErrorBody      = [8]byte{}
+	wccDataErrorFormatter = errFormatterWithBody(wccDataErrorBody[:])
+)
diff --git a/pkg/go-nfs/example/helloworld/main.go b/pkg/go-nfs/example/helloworld/main.go
new file mode 100644
index 0000000..87f2d98
--- /dev/null
+++ b/pkg/go-nfs/example/helloworld/main.go
@@ -0,0 +1,52 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"net"
+
+	"github.com/go-git/go-billy/v5"
+	"github.com/go-git/go-billy/v5/memfs"
+
+	nfs "git.kmsign.ru/royalcat/tstor/pkg/go-nfs"
+	"git.kmsign.ru/royalcat/tstor/pkg/go-nfs/helpers"
+	nfshelper "git.kmsign.ru/royalcat/tstor/pkg/go-nfs/helpers"
+)
+
+// ROFS is an intercepter for the filesystem indicating it should
+// be read only. The undelrying billy.Memfs indicates it supports
+// writing, but does not in implement billy.Change to support
+// modification of permissions / modTimes, and as such cannot be
+// used as RW system.
+type ROFS struct {
+	nfs.Filesystem
+}
+
+// Capabilities exports the filesystem as readonly
+func (ROFS) Capabilities() billy.Capability {
+	return billy.ReadCapability | billy.SeekCapability
+}
+
+func main() {
+	ctx := context.Background()
+
+	listener, err := net.Listen("tcp", ":0")
+	if err != nil {
+		fmt.Printf("Failed to listen: %v\n", err)
+		return
+	}
+	fmt.Printf("Server running at %s\n", listener.Addr())
+
+	mem := helpers.WrapBillyFS(memfs.New())
+	f, err := mem.Create(ctx, "hello.txt")
+	if err != nil {
+		fmt.Printf("Failed to create file: %v\n", err)
+		return
+	}
+	_, _ = f.Write(ctx, []byte("hello world"))
+	_ = f.Close(ctx)
+
+	handler := nfshelper.NewNullAuthHandler(ROFS{mem})
+	cacheHelper := nfshelper.NewCachingHandler(handler, 1024)
+	fmt.Printf("%v", nfs.Serve(listener, cacheHelper))
+}
diff --git a/pkg/go-nfs/example/osnfs/changeos.go b/pkg/go-nfs/example/osnfs/changeos.go
new file mode 100644
index 0000000..38ca4d3
--- /dev/null
+++ b/pkg/go-nfs/example/osnfs/changeos.go
@@ -0,0 +1,38 @@
+package main
+
+import (
+	"os"
+	"time"
+
+	"github.com/go-git/go-billy/v5"
+)
+
+// NewChangeOSFS wraps billy osfs to add the change interface
+func NewChangeOSFS(fs billy.Filesystem) billy.Filesystem {
+	return COS{fs}
+}
+
+// COS or OSFS + Change wraps a billy.FS to not fail the `Change` interface.
+type COS struct {
+	billy.Filesystem
+}
+
+// Chmod changes mode
+func (fs COS) Chmod(name string, mode os.FileMode) error {
+	return os.Chmod(fs.Join(fs.Root(), name), mode)
+}
+
+// Lchown changes ownership
+func (fs COS) Lchown(name string, uid, gid int) error {
+	return os.Lchown(fs.Join(fs.Root(), name), uid, gid)
+}
+
+// Chown changes ownership
+func (fs COS) Chown(name string, uid, gid int) error {
+	return os.Chown(fs.Join(fs.Root(), name), uid, gid)
+}
+
+// Chtimes changes access time
+func (fs COS) Chtimes(name string, atime time.Time, mtime time.Time) error {
+	return os.Chtimes(fs.Join(fs.Root(), name), atime, mtime)
+}
diff --git a/pkg/go-nfs/example/osnfs/changeos_unix.go b/pkg/go-nfs/example/osnfs/changeos_unix.go
new file mode 100644
index 0000000..2fe93c6
--- /dev/null
+++ b/pkg/go-nfs/example/osnfs/changeos_unix.go
@@ -0,0 +1,28 @@
+//go:build darwin || dragonfly || freebsd || linux || nacl || netbsd || openbsd || solaris
+
+package main
+
+import (
+	"golang.org/x/sys/unix"
+)
+
+func (fs COS) Mknod(path string, mode uint32, major uint32, minor uint32) error {
+	dev := unix.Mkdev(major, minor)
+	return unix.Mknod(fs.Join(fs.Root(), path), mode, int(dev))
+}
+
+func (fs COS) Mkfifo(path string, mode uint32) error {
+	return unix.Mkfifo(fs.Join(fs.Root(), path), mode)
+}
+
+func (fs COS) Link(path string, link string) error {
+	return unix.Link(fs.Join(fs.Root(), path), link)
+}
+
+func (fs COS) Socket(path string) error {
+	fd, err := unix.Socket(unix.AF_UNIX, unix.SOCK_STREAM, 0)
+	if err != nil {
+		return err
+	}
+	return unix.Bind(fd, &unix.SockaddrUnix{Name: fs.Join(fs.Root(), path)})
+}
diff --git a/pkg/go-nfs/example/osnfs/main.go b/pkg/go-nfs/example/osnfs/main.go
new file mode 100644
index 0000000..feadccc
--- /dev/null
+++ b/pkg/go-nfs/example/osnfs/main.go
@@ -0,0 +1,36 @@
+package main
+
+import (
+	"fmt"
+	"net"
+	"os"
+
+	nfs "git.kmsign.ru/royalcat/tstor/pkg/go-nfs"
+	"git.kmsign.ru/royalcat/tstor/pkg/go-nfs/helpers"
+	nfshelper "git.kmsign.ru/royalcat/tstor/pkg/go-nfs/helpers"
+	osfs "github.com/go-git/go-billy/v5/osfs"
+)
+
+func main() {
+	port := ""
+	if len(os.Args) < 2 {
+		fmt.Printf("Usage: osnfs </path/to/folder> [port]\n")
+		return
+	} else if len(os.Args) == 3 {
+		port = os.Args[2]
+	}
+
+	listener, err := net.Listen("tcp", ":"+port)
+	if err != nil {
+		fmt.Printf("Failed to listen: %v\n", err)
+		return
+	}
+	fmt.Printf("osnfs server running at %s\n", listener.Addr())
+
+	bfs := osfs.New(os.Args[1])
+	bfsPlusChange := helpers.WrapBillyFS(NewChangeOSFS(bfs))
+
+	handler := nfshelper.NewNullAuthHandler(bfsPlusChange)
+	cacheHelper := nfshelper.NewCachingHandler(handler, 1024)
+	fmt.Printf("%v", nfs.Serve(listener, cacheHelper))
+}
diff --git a/pkg/go-nfs/example/osview/main.go b/pkg/go-nfs/example/osview/main.go
new file mode 100644
index 0000000..355df17
--- /dev/null
+++ b/pkg/go-nfs/example/osview/main.go
@@ -0,0 +1,37 @@
+package main
+
+import (
+	"fmt"
+	"net"
+	"os"
+
+	"github.com/willscott/memphis"
+
+	nfs "git.kmsign.ru/royalcat/tstor/pkg/go-nfs"
+	"git.kmsign.ru/royalcat/tstor/pkg/go-nfs/helpers"
+	nfshelper "git.kmsign.ru/royalcat/tstor/pkg/go-nfs/helpers"
+)
+
+func main() {
+	port := ""
+	if len(os.Args) < 2 {
+		fmt.Printf("Usage: osview </path/to/folder> [port]\n")
+		return
+	} else if len(os.Args) == 3 {
+		port = os.Args[2]
+	}
+
+	listener, err := net.Listen("tcp", ":"+port)
+	if err != nil {
+		fmt.Printf("Failed to listen: %v\n", err)
+		return
+	}
+	fmt.Printf("Server running at %s\n", listener.Addr())
+
+	fs := memphis.FromOS(os.Args[1])
+	bfs := helpers.WrapBillyFS(fs.AsBillyFS(0, 0))
+
+	handler := nfshelper.NewNullAuthHandler(bfs)
+	cacheHelper := nfshelper.NewCachingHandler(handler, 1024)
+	fmt.Printf("%v", nfs.Serve(listener, cacheHelper))
+}
diff --git a/pkg/go-nfs/file.go b/pkg/go-nfs/file.go
new file mode 100644
index 0000000..2a108b1
--- /dev/null
+++ b/pkg/go-nfs/file.go
@@ -0,0 +1,377 @@
+package nfs
+
+import (
+	"context"
+	"errors"
+	"hash/fnv"
+	"io"
+	"math"
+	"os"
+	"time"
+
+	"git.kmsign.ru/royalcat/tstor/pkg/go-nfs/file"
+	"github.com/willscott/go-nfs-client/nfs/xdr"
+)
+
+// FileAttribute holds metadata about a filesystem object
+type FileAttribute struct {
+	Type                FileType
+	FileMode            uint32
+	Nlink               uint32
+	UID                 uint32
+	GID                 uint32
+	Filesize            uint64
+	Used                uint64
+	SpecData            [2]uint32
+	FSID                uint64
+	Fileid              uint64
+	Atime, Mtime, Ctime FileTime
+}
+
+// FileType represents a NFS File Type
+type FileType uint32
+
+// Enumeration of NFS FileTypes
+const (
+	FileTypeRegular FileType = iota + 1
+	FileTypeDirectory
+	FileTypeBlock
+	FileTypeCharacter
+	FileTypeLink
+	FileTypeSocket
+	FileTypeFIFO
+)
+
+func (f FileType) String() string {
+	switch f {
+	case FileTypeRegular:
+		return "Regular"
+	case FileTypeDirectory:
+		return "Directory"
+	case FileTypeBlock:
+		return "Block Device"
+	case FileTypeCharacter:
+		return "Character Device"
+	case FileTypeLink:
+		return "Symbolic Link"
+	case FileTypeSocket:
+		return "Socket"
+	case FileTypeFIFO:
+		return "FIFO"
+	default:
+		return "Unknown"
+	}
+}
+
+// Mode provides the OS interpreted mode of the file attributes
+func (f *FileAttribute) Mode() os.FileMode {
+	return os.FileMode(f.FileMode)
+}
+
+// FileCacheAttribute is the subset of FileAttribute used by
+// wcc_attr
+type FileCacheAttribute struct {
+	Filesize     uint64
+	Mtime, Ctime FileTime
+}
+
+// AsCache provides the wcc view of the file attributes
+func (f FileAttribute) AsCache() *FileCacheAttribute {
+	wcc := FileCacheAttribute{
+		Filesize: f.Filesize,
+		Mtime:    f.Mtime,
+		Ctime:    f.Ctime,
+	}
+	return &wcc
+}
+
+// ToFileAttribute creates an NFS fattr3 struct from an OS.FileInfo
+func ToFileAttribute(info os.FileInfo, filePath string) *FileAttribute {
+	f := FileAttribute{}
+
+	m := info.Mode()
+	f.FileMode = uint32(m)
+	if info.IsDir() {
+		f.Type = FileTypeDirectory
+	} else if m&os.ModeSymlink != 0 {
+		f.Type = FileTypeLink
+	} else if m&os.ModeCharDevice != 0 {
+		f.Type = FileTypeCharacter
+	} else if m&os.ModeDevice != 0 {
+		f.Type = FileTypeBlock
+	} else if m&os.ModeSocket != 0 {
+		f.Type = FileTypeSocket
+	} else if m&os.ModeNamedPipe != 0 {
+		f.Type = FileTypeFIFO
+	} else {
+		f.Type = FileTypeRegular
+	}
+	// The number of hard links to the file.
+	f.Nlink = 1
+
+	if a := file.GetInfo(info); a != nil {
+		f.Nlink = a.Nlink
+		f.UID = a.UID
+		f.GID = a.GID
+		f.SpecData = [2]uint32{a.Major, a.Minor}
+		f.Fileid = a.Fileid
+	} else {
+		hasher := fnv.New64()
+		_, _ = hasher.Write([]byte(filePath))
+		f.Fileid = hasher.Sum64()
+	}
+
+	f.Filesize = uint64(info.Size())
+	f.Used = uint64(info.Size())
+	f.Atime = ToNFSTime(info.ModTime())
+	f.Mtime = f.Atime
+	f.Ctime = f.Atime
+	return &f
+}
+
+// tryStat attempts to create a FileAttribute from a path.
+func tryStat(ctx context.Context, fs Filesystem, path []string) *FileAttribute {
+	fullPath := fs.Join(path...)
+	attrs, err := fs.Lstat(ctx, fullPath)
+	if err != nil || attrs == nil {
+		Log.Errorf("err loading attrs for %s: %v", fs.Join(path...), err)
+		return nil
+	}
+	return ToFileAttribute(attrs, fullPath)
+}
+
+// WriteWcc writes the `wcc_data` representation of an object.
+func WriteWcc(writer io.Writer, pre *FileCacheAttribute, post *FileAttribute) error {
+	if pre == nil {
+		if err := xdr.Write(writer, uint32(0)); err != nil {
+			return err
+		}
+	} else {
+		if err := xdr.Write(writer, uint32(1)); err != nil {
+			return err
+		}
+		if err := xdr.Write(writer, *pre); err != nil {
+			return err
+		}
+	}
+	if post == nil {
+		if err := xdr.Write(writer, uint32(0)); err != nil {
+			return err
+		}
+	} else {
+		if err := xdr.Write(writer, uint32(1)); err != nil {
+			return err
+		}
+		if err := xdr.Write(writer, *post); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// WritePostOpAttrs writes the `post_op_attr` representation of a files attributes
+func WritePostOpAttrs(writer io.Writer, post *FileAttribute) error {
+	if post == nil {
+		if err := xdr.Write(writer, uint32(0)); err != nil {
+			return err
+		}
+	} else {
+		if err := xdr.Write(writer, uint32(1)); err != nil {
+			return err
+		}
+		if err := xdr.Write(writer, *post); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// SetFileAttributes represents a command to update some metadata
+// about a file.
+type SetFileAttributes struct {
+	SetMode  *uint32
+	SetUID   *uint32
+	SetGID   *uint32
+	SetSize  *uint64
+	SetAtime *time.Time
+	SetMtime *time.Time
+}
+
+// Apply uses a `Change` implementation to set defined attributes on a
+// provided file.
+func (s *SetFileAttributes) Apply(ctx context.Context, changer Change, fs Filesystem, file string) error {
+	curOS, err := fs.Lstat(ctx, file)
+	if errors.Is(err, os.ErrNotExist) {
+		return &NFSStatusError{NFSStatusNoEnt, os.ErrNotExist}
+	} else if errors.Is(err, os.ErrPermission) {
+		return &NFSStatusError{NFSStatusAccess, os.ErrPermission}
+	} else if err != nil {
+		return nil
+	}
+	curr := ToFileAttribute(curOS, file)
+
+	if s.SetMode != nil {
+		mode := os.FileMode(*s.SetMode) & os.ModePerm
+		if mode != curr.Mode().Perm() {
+			if changer == nil {
+				return &NFSStatusError{NFSStatusNotSupp, os.ErrPermission}
+			}
+			if err := changer.Chmod(ctx, file, mode); err != nil {
+				if errors.Is(err, os.ErrPermission) {
+					return &NFSStatusError{NFSStatusAccess, os.ErrPermission}
+				}
+				return err
+			}
+		}
+	}
+	if s.SetUID != nil || s.SetGID != nil {
+		euid := curr.UID
+		if s.SetUID != nil {
+			euid = *s.SetUID
+		}
+		egid := curr.GID
+		if s.SetGID != nil {
+			egid = *s.SetGID
+		}
+		if euid != curr.UID || egid != curr.GID {
+			if changer == nil {
+				return &NFSStatusError{NFSStatusNotSupp, os.ErrPermission}
+			}
+			if err := changer.Lchown(ctx, file, int(euid), int(egid)); err != nil {
+				if errors.Is(err, os.ErrPermission) {
+					return &NFSStatusError{NFSStatusAccess, os.ErrPermission}
+				}
+				return err
+			}
+		}
+	}
+	if s.SetSize != nil {
+		if curr.Mode()&os.ModeSymlink != 0 {
+			return &NFSStatusError{NFSStatusNotSupp, os.ErrInvalid}
+		}
+		fp, err := fs.OpenFile(ctx, file, os.O_WRONLY|os.O_EXCL, 0)
+		if errors.Is(err, os.ErrPermission) {
+			return &NFSStatusError{NFSStatusAccess, err}
+		} else if err != nil {
+			return err
+		}
+		if *s.SetSize > math.MaxInt64 {
+			return &NFSStatusError{NFSStatusInval, os.ErrInvalid}
+		}
+		if err := fp.Truncate(ctx, int64(*s.SetSize)); err != nil {
+			return err
+		}
+		if err := fp.Close(ctx); err != nil {
+			return err
+		}
+	}
+
+	if s.SetAtime != nil || s.SetMtime != nil {
+		atime := curr.Atime.Native()
+		if s.SetAtime != nil {
+			atime = s.SetAtime
+		}
+		mtime := curr.Mtime.Native()
+		if s.SetMtime != nil {
+			mtime = s.SetMtime
+		}
+		if atime != curr.Atime.Native() || mtime != curr.Mtime.Native() {
+			if changer == nil {
+				return &NFSStatusError{NFSStatusNotSupp, os.ErrPermission}
+			}
+			if err := changer.Chtimes(ctx, file, *atime, *mtime); err != nil {
+				if errors.Is(err, os.ErrPermission) {
+					return &NFSStatusError{NFSStatusAccess, err}
+				}
+				return err
+			}
+		}
+	}
+	return nil
+}
+
+// Mode returns a mode if specified or the provided default mode.
+func (s *SetFileAttributes) Mode(def os.FileMode) os.FileMode {
+	if s.SetMode != nil {
+		return os.FileMode(*s.SetMode) & os.ModePerm
+	}
+	return def
+}
+
+// ReadSetFileAttributes reads an sattr3 xdr stream into a go struct.
+func ReadSetFileAttributes(r io.Reader) (*SetFileAttributes, error) {
+	attrs := SetFileAttributes{}
+	hasMode, err := xdr.ReadUint32(r)
+	if err != nil {
+		return nil, err
+	}
+	if hasMode != 0 {
+		mode, err := xdr.ReadUint32(r)
+		if err != nil {
+			return nil, err
+		}
+		attrs.SetMode = &mode
+	}
+	hasUID, err := xdr.ReadUint32(r)
+	if err != nil {
+		return nil, err
+	}
+	if hasUID != 0 {
+		uid, err := xdr.ReadUint32(r)
+		if err != nil {
+			return nil, err
+		}
+		attrs.SetUID = &uid
+	}
+	hasGID, err := xdr.ReadUint32(r)
+	if err != nil {
+		return nil, err
+	}
+	if hasGID != 0 {
+		gid, err := xdr.ReadUint32(r)
+		if err != nil {
+			return nil, err
+		}
+		attrs.SetGID = &gid
+	}
+	hasSize, err := xdr.ReadUint32(r)
+	if err != nil {
+		return nil, err
+	}
+	if hasSize != 0 {
+		var size uint64
+		attrs.SetSize = &size
+		if err := xdr.Read(r, &size); err != nil {
+			return nil, err
+		}
+	}
+	aTime, err := xdr.ReadUint32(r)
+	if err != nil {
+		return nil, err
+	}
+	if aTime == 1 {
+		now := time.Now()
+		attrs.SetAtime = &now
+	} else if aTime == 2 {
+		t := FileTime{}
+		if err := xdr.Read(r, &t); err != nil {
+			return nil, err
+		}
+		attrs.SetAtime = t.Native()
+	}
+	mTime, err := xdr.ReadUint32(r)
+	if err != nil {
+		return nil, err
+	}
+	if mTime == 1 {
+		now := time.Now()
+		attrs.SetMtime = &now
+	} else if mTime == 2 {
+		t := FileTime{}
+		if err := xdr.Read(r, &t); err != nil {
+			return nil, err
+		}
+		attrs.SetMtime = t.Native()
+	}
+	return &attrs, nil
+}
diff --git a/pkg/go-nfs/file/file.go b/pkg/go-nfs/file/file.go
new file mode 100644
index 0000000..2853868
--- /dev/null
+++ b/pkg/go-nfs/file/file.go
@@ -0,0 +1,17 @@
+package file
+
+import "os"
+
+type FileInfo struct {
+	Nlink  uint32
+	UID    uint32
+	GID    uint32
+	Major  uint32
+	Minor  uint32
+	Fileid uint64
+}
+
+// GetInfo extracts some non-standardized items from the result of a Stat call.
+func GetInfo(fi os.FileInfo) *FileInfo {
+	return getInfo(fi)
+}
diff --git a/pkg/go-nfs/file/file_unix.go b/pkg/go-nfs/file/file_unix.go
new file mode 100644
index 0000000..6658c20
--- /dev/null
+++ b/pkg/go-nfs/file/file_unix.go
@@ -0,0 +1,24 @@
+//go:build darwin || dragonfly || freebsd || linux || nacl || netbsd || openbsd || solaris
+
+package file
+
+import (
+	"os"
+	"syscall"
+
+	"golang.org/x/sys/unix"
+)
+
+func getInfo(info os.FileInfo) *FileInfo {
+	fi := &FileInfo{}
+	if s, ok := info.Sys().(*syscall.Stat_t); ok {
+		fi.Nlink = uint32(s.Nlink)
+		fi.UID = s.Uid
+		fi.GID = s.Gid
+		fi.Major = unix.Major(uint64(s.Rdev))
+		fi.Minor = unix.Minor(uint64(s.Rdev))
+		fi.Fileid = s.Ino
+		return fi
+	}
+	return nil
+}
diff --git a/pkg/go-nfs/file/file_windows.go b/pkg/go-nfs/file/file_windows.go
new file mode 100644
index 0000000..ef173d5
--- /dev/null
+++ b/pkg/go-nfs/file/file_windows.go
@@ -0,0 +1,12 @@
+//go:build windows
+
+package file
+
+import "os"
+
+func getInfo(info os.FileInfo) *FileInfo {
+	// https://godoc.org/golang.org/x/sys/windows#GetFileInformationByHandle
+	// can be potentially used to populate Nlink
+
+	return nil
+}
diff --git a/pkg/go-nfs/filesystem.go b/pkg/go-nfs/filesystem.go
new file mode 100644
index 0000000..3a06cc8
--- /dev/null
+++ b/pkg/go-nfs/filesystem.go
@@ -0,0 +1,101 @@
+package nfs
+
+import (
+	"context"
+	"io"
+	"os"
+	"time"
+
+	"git.kmsign.ru/royalcat/tstor/pkg/ctxio"
+)
+
+// FSStat returns metadata about a file system
+type FSStat struct {
+	TotalSize      uint64
+	FreeSize       uint64
+	AvailableSize  uint64
+	TotalFiles     uint64
+	FreeFiles      uint64
+	AvailableFiles uint64
+	// CacheHint is called "invarsec" in the nfs standard
+	CacheHint time.Duration
+}
+
+type Filesystem interface {
+	// Create creates the named file with mode 0666 (before umask), truncating
+	// it if it already exists. If successful, methods on the returned File can
+	// be used for I/O; the associated file descriptor has mode O_RDWR.
+	Create(ctx context.Context, filename string) (File, error)
+	// Open opens the named file for reading. If successful, methods on the
+	// returned file can be used for reading; the associated file descriptor has
+	// mode O_RDONLY.
+	Open(ctx context.Context, filename string) (File, error)
+	// OpenFile is the generalized open call; most users will use Open or Create
+	// instead. It opens the named file with specified flag (O_RDONLY etc.) and
+	// perm, (0666 etc.) if applicable. If successful, methods on the returned
+	// File can be used for I/O.
+	OpenFile(ctx context.Context, filename string, flag int, perm os.FileMode) (File, error)
+	// Stat returns a FileInfo describing the named file.
+	Stat(ctx context.Context, filename string) (os.FileInfo, error)
+	// Rename renames (moves) oldpath to newpath. If newpath already exists and
+	// is not a directory, Rename replaces it. OS-specific restrictions may
+	// apply when oldpath and newpath are in different directories.
+	Rename(ctx context.Context, oldpath, newpath string) error
+	// Remove removes the named file or directory.
+	Remove(ctx context.Context, filename string) error
+	// Join joins any number of path elements into a single path, adding a
+	// Separator if necessary. Join calls filepath.Clean on the result; in
+	// particular, all empty strings are ignored. On Windows, the result is a
+	// UNC path if and only if the first path element is a UNC path.
+	Join(elem ...string) string
+
+	// ReadDir reads the directory named by d(irname and returns a list of
+	// directory entries sorted by filename.
+	ReadDir(ctx context.Context, path string) ([]os.FileInfo, error)
+	// MkdirAll creates a directory named path, along with any necessary
+	// parents, and returns nil, or else returns an error. The permission bits
+	// perm are used for all directories that MkdirAll creates. If path is/
+	// already a directory, MkdirAll does nothing and returns nil.
+	MkdirAll(ctx context.Context, filename string, perm os.FileMode) error
+
+	// Lstat returns a FileInfo describing the named file. If the file is a
+	// symbolic link, the returned FileInfo describes the symbolic link. Lstat
+	// makes no attempt to follow the link.
+	Lstat(ctx context.Context, filename string) (os.FileInfo, error)
+	// Symlink creates a symbolic-link from link to target. target may be an
+	// absolute or relative path, and need not refer to an existing node.
+	// Parent directories of link are created as necessary.
+	Symlink(ctx context.Context, target, link string) error
+	// Readlink returns the target path of link.
+	Readlink(ctx context.Context, link string) (string, error)
+}
+
+type File interface {
+	// Name returns the name of the file as presented to Open.
+	Name() string
+	ctxio.Writer
+	ctxio.Reader
+	ctxio.ReaderAt
+	io.Seeker
+	ctxio.Closer
+
+	// Truncate the file.
+	Truncate(ctx context.Context, size int64) error
+}
+
+// Change abstract the FileInfo change related operations in a storage-agnostic
+// interface as an extension to the Basic interface
+type Change interface {
+	// Chmod changes the mode of the named file to mode. If the file is a
+	// symbolic link, it changes the mode of the link's target.
+	Chmod(ctx context.Context, name string, mode os.FileMode) error
+	// Lchown changes the numeric uid and gid of the named file. If the file is
+	// a symbolic link, it changes the uid and gid of the link itself.
+	Lchown(ctx context.Context, name string, uid, gid int) error
+	// Chtimes changes the access and modification times of the named file,
+	// similar to the Unix utime() or utimes() functions.
+	//
+	// The underlying filesystem may truncate or round the values to a less
+	// precise time unit.
+	Chtimes(ctx context.Context, name string, atime time.Time, mtime time.Time) error
+}
diff --git a/pkg/go-nfs/handler.go b/pkg/go-nfs/handler.go
new file mode 100644
index 0000000..cac4d72
--- /dev/null
+++ b/pkg/go-nfs/handler.go
@@ -0,0 +1,52 @@
+package nfs
+
+import (
+	"context"
+	"io/fs"
+	"net"
+
+	"git.kmsign.ru/royalcat/tstor/pkg/ctxbilly"
+)
+
+// Handler represents the interface of the file system / vfs being exposed over NFS
+type Handler interface {
+	// Required methods
+
+	Mount(context.Context, net.Conn, MountRequest) (MountStatus, Filesystem, []AuthFlavor)
+
+	// Change can return 'nil' if filesystem is read-only
+	// If the returned value can be cast to `UnixChange`, mknod and link RPCs will be available.
+	Change(Filesystem) Change
+
+	// Optional methods - generic helpers or trivial implementations can be sufficient depending on use case.
+
+	// Fill in information about a file system's free space.
+	FSStat(context.Context, Filesystem, *FSStat) error
+
+	// represent file objects as opaque references
+	// Can be safely implemented via helpers/cachinghandler.
+	ToHandle(fs Filesystem, path []string) []byte
+	FromHandle(fh []byte) (Filesystem, []string, error)
+	InvalidateHandle(Filesystem, []byte) error
+
+	// How many handles can be safely maintained by the handler.
+	HandleLimit() int
+}
+
+// UnixChange extends the billy `Change` interface with support for special files.
+type UnixChange interface {
+	ctxbilly.Change
+	Mknod(ctx context.Context, path string, mode uint32, major uint32, minor uint32) error
+	Mkfifo(ctx context.Context, path string, mode uint32) error
+	Socket(ctx context.Context, path string) error
+	Link(ctx context.Context, path string, link string) error
+}
+
+// CachingHandler represents the optional caching work that a user may wish to over-ride with
+// their own implementations, but which can be otherwise provided through defaults.
+type CachingHandler interface {
+	VerifierFor(path string, contents []fs.FileInfo) uint64
+
+	// fs.FileInfo needs to be sorted by Name(), nil in case of a cache-miss
+	DataForVerifier(path string, verifier uint64) []fs.FileInfo
+}
diff --git a/pkg/go-nfs/helpers/billlyfs.go b/pkg/go-nfs/helpers/billlyfs.go
new file mode 100644
index 0000000..3dd3c2c
--- /dev/null
+++ b/pkg/go-nfs/helpers/billlyfs.go
@@ -0,0 +1,157 @@
+package helpers
+
+import (
+	"context"
+	"io/fs"
+
+	"git.kmsign.ru/royalcat/tstor/pkg/go-nfs"
+	"github.com/go-git/go-billy/v5"
+)
+
+func WrapBillyFS(bf billy.Filesystem) nfs.Filesystem {
+	return &wrapFS{
+		Filesystem: bf,
+	}
+}
+
+type wrapFS struct {
+	billy.Filesystem
+}
+
+var _ nfs.Filesystem = (*wrapFS)(nil)
+
+// Create implements Filesystem.
+// Subtle: this method shadows the method (Filesystem).Create of MemFS.Filesystem.
+func (m *wrapFS) Create(ctx context.Context, filename string) (nfs.File, error) {
+	bf, err := m.Filesystem.Create(filename)
+	if err != nil {
+		return nil, err
+	}
+	return &wrapFile{bf}, nil
+}
+
+// Lstat implements Filesystem.
+// Subtle: this method shadows the method (Filesystem).Lstat of MemFS.Filesystem.
+func (m *wrapFS) Lstat(ctx context.Context, filename string) (fs.FileInfo, error) {
+	return m.Filesystem.Lstat(filename)
+}
+
+// MkdirAll implements Filesystem.
+// Subtle: this method shadows the method (Filesystem).MkdirAll of MemFS.Filesystem.
+func (m *wrapFS) MkdirAll(ctx context.Context, filename string, perm fs.FileMode) error {
+	return m.Filesystem.MkdirAll(filename, perm)
+}
+
+// Open implements Filesystem.
+// Subtle: this method shadows the method (Filesystem).Open of MemFS.Filesystem.
+func (m *wrapFS) Open(ctx context.Context, filename string) (nfs.File, error) {
+	bf, err := m.Filesystem.Open(filename)
+	if err != nil {
+		return nil, err
+	}
+	return WrapFile(bf), nil
+}
+
+// OpenFile implements Filesystem.
+// Subtle: this method shadows the method (Filesystem).OpenFile of MemFS.Filesystem.
+func (m *wrapFS) OpenFile(ctx context.Context, filename string, flag int, perm fs.FileMode) (nfs.File, error) {
+	bf, err := m.Filesystem.OpenFile(filename, flag, perm)
+	if err != nil {
+		return nil, err
+	}
+	return WrapFile(bf), nil
+}
+
+// ReadDir implements Filesystem.
+// Subtle: this method shadows the method (Filesystem).ReadDir of MemFS.Filesystem.
+func (m *wrapFS) ReadDir(ctx context.Context, path string) ([]fs.FileInfo, error) {
+	return m.Filesystem.ReadDir(path)
+}
+
+// Readlink implements Filesystem.
+// Subtle: this method shadows the method (Filesystem).Readlink of MemFS.Filesystem.
+func (m *wrapFS) Readlink(ctx context.Context, link string) (string, error) {
+	return m.Filesystem.Readlink(link)
+}
+
+// Remove implements Filesystem.
+// Subtle: this method shadows the method (Filesystem).Remove of MemFS.Filesystem.
+func (m *wrapFS) Remove(ctx context.Context, filename string) error {
+	return m.Filesystem.Remove(filename)
+}
+
+// Rename implements Filesystem.
+// Subtle: this method shadows the method (Filesystem).Rename of MemFS.Filesystem.
+func (m *wrapFS) Rename(ctx context.Context, oldpath string, newpath string) error {
+	return m.Filesystem.Rename(oldpath, newpath)
+}
+
+// Stat implements Filesystem.
+// Subtle: this method shadows the method (Filesystem).Stat of MemFS.Filesystem.
+func (m *wrapFS) Stat(ctx context.Context, filename string) (fs.FileInfo, error) {
+	return m.Filesystem.Stat(filename)
+}
+
+// Symlink implements Filesystem.
+// Subtle: this method shadows the method (Filesystem).Symlink of MemFS.Filesystem.
+func (m *wrapFS) Symlink(ctx context.Context, target string, link string) error {
+	return m.Filesystem.Symlink(target, link)
+}
+
+func WrapFile(bf billy.File) nfs.File {
+	return &wrapFile{File: bf}
+}
+
+type wrapFile struct {
+	billy.File
+}
+
+var _ nfs.File = (*wrapFile)(nil)
+
+// Close implements File.
+// Subtle: this method shadows the method (File).Close of MemFile.File.
+func (m *wrapFile) Close(ctx context.Context) error {
+	return m.File.Close()
+}
+
+// Lock implements File.
+// Subtle: this method shadows the method (File).Lock of MemFile.File.
+func (m *wrapFile) Lock() error {
+	return m.File.Lock()
+}
+
+// Name implements File.
+// Subtle: this method shadows the method (File).Name of MemFile.File.
+func (m *wrapFile) Name() string {
+	return m.File.Name()
+}
+
+// Truncate implements File.
+// Subtle: this method shadows the method (File).Truncate of memFile.File.
+func (m *wrapFile) Truncate(ctx context.Context, size int64) error {
+	return m.File.Truncate(size)
+}
+
+// Read implements File.
+// Subtle: this method shadows the method (File).Read of MemFile.File.
+func (m *wrapFile) Read(ctx context.Context, p []byte) (n int, err error) {
+	return m.File.Read(p)
+}
+
+// ReadAt implements File.
+// Subtle: this method shadows the method (File).ReadAt of MemFile.File.
+func (m *wrapFile) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) {
+	return m.File.ReadAt(p, off)
+}
+
+// Unlock implements File.
+// Subtle: this method shadows the method (File).Unlock of MemFile.File.
+func (m *wrapFile) Unlock() error {
+	return m.File.Unlock()
+}
+
+// Write implements File.
+// Subtle: this method shadows the method (File).Write of MemFile.File.
+func (m *wrapFile) Write(ctx context.Context, p []byte) (n int, err error) {
+	return m.File.Write(p)
+}
diff --git a/src/export/nfs/cache.go b/pkg/go-nfs/helpers/cachinghandler.go
similarity index 90%
rename from src/export/nfs/cache.go
rename to pkg/go-nfs/helpers/cachinghandler.go
index 926bbda..06d5934 100644
--- a/src/export/nfs/cache.go
+++ b/pkg/go-nfs/helpers/cachinghandler.go
@@ -1,15 +1,13 @@
-package nfs
+package helpers
 
 import (
 	"crypto/sha256"
 	"encoding/binary"
 	"io/fs"
 	"reflect"
-	"slices"
 
-	"github.com/willscott/go-nfs"
+	"git.kmsign.ru/royalcat/tstor/pkg/go-nfs"
 
-	"github.com/go-git/go-billy/v5"
 	"github.com/google/uuid"
 	lru "github.com/hashicorp/golang-lru/v2"
 )
@@ -46,14 +44,14 @@ type CachingHandler struct {
 }
 
 type entry struct {
-	f billy.Filesystem
+	f nfs.Filesystem
 	p []string
 }
 
 // ToHandle takes a file and represents it with an opaque handle to reference it.
 // In stateless nfs (when it's serving a unix fs) this can be the device + inode
 // but we can generalize with a stateful local cache of handed out IDs.
-func (c *CachingHandler) ToHandle(f billy.Filesystem, path []string) []byte {
+func (c *CachingHandler) ToHandle(f nfs.Filesystem, path []string) []byte {
 	joinedPath := f.Join(path...)
 
 	if handle := c.searchReverseCache(f, joinedPath); handle != nil {
@@ -81,7 +79,7 @@ func (c *CachingHandler) ToHandle(f billy.Filesystem, path []string) []byte {
 }
 
 // FromHandle converts from an opaque handle to the file it represents
-func (c *CachingHandler) FromHandle(fh []byte) (billy.Filesystem, []string, error) {
+func (c *CachingHandler) FromHandle(fh []byte) (nfs.Filesystem, []string, error) {
 	id, err := uuid.FromBytes(fh)
 	if err != nil {
 		return nil, []string{}, err
@@ -94,13 +92,16 @@ func (c *CachingHandler) FromHandle(fh []byte) (billy.Filesystem, []string, erro
 				_, _ = c.activeHandles.Get(k)
 			}
 		}
-
-		return f.f, slices.Clone(f.p), nil
+		if ok {
+			newP := make([]string, len(f.p))
+			copy(newP, f.p)
+			return f.f, newP, nil
+		}
 	}
 	return nil, []string{}, &nfs.NFSStatusError{NFSStatus: nfs.NFSStatusStale}
 }
 
-func (c *CachingHandler) searchReverseCache(f billy.Filesystem, path string) []byte {
+func (c *CachingHandler) searchReverseCache(f nfs.Filesystem, path string) []byte {
 	uuids, exists := c.reverseHandles[path]
 
 	if !exists {
@@ -133,7 +134,7 @@ func (c *CachingHandler) evictReverseCache(path string, handle uuid.UUID) {
 	}
 }
 
-func (c *CachingHandler) InvalidateHandle(fs billy.Filesystem, handle []byte) error {
+func (c *CachingHandler) InvalidateHandle(fs nfs.Filesystem, handle []byte) error {
 	//Remove from cache
 	id, _ := uuid.FromBytes(handle)
 	entry, ok := c.activeHandles.Get(id)
diff --git a/pkg/go-nfs/helpers/memfs/memfs.go b/pkg/go-nfs/helpers/memfs/memfs.go
new file mode 100644
index 0000000..5e1822a
--- /dev/null
+++ b/pkg/go-nfs/helpers/memfs/memfs.go
@@ -0,0 +1,414 @@
+// Package memfs is a variant of "github.com/go-git/go-billy/v5/memfs" with
+// stable mtimes for items.
+package memfs
+
+import (
+	"errors"
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+	"sort"
+	"strings"
+	"syscall"
+	"time"
+
+	"github.com/go-git/go-billy/v5"
+	"github.com/go-git/go-billy/v5/helper/chroot"
+	"github.com/go-git/go-billy/v5/util"
+)
+
+const separator = filepath.Separator
+
+// Memory a very convenient filesystem based on memory files
+type Memory struct {
+	s *storage
+}
+
+// New returns a new Memory filesystem.
+func New() billy.Filesystem {
+	fs := &Memory{s: newStorage()}
+	return chroot.New(fs, string(separator))
+}
+
+func (fs *Memory) Create(filename string) (billy.File, error) {
+	return fs.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
+}
+
+func (fs *Memory) Open(filename string) (billy.File, error) {
+	return fs.OpenFile(filename, os.O_RDONLY, 0)
+}
+
+func (fs *Memory) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) {
+	f, has := fs.s.Get(filename)
+	if !has {
+		if !isCreate(flag) {
+			return nil, os.ErrNotExist
+		}
+
+		var err error
+		f, err = fs.s.New(filename, perm, flag)
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		if isExclusive(flag) {
+			return nil, os.ErrExist
+		}
+
+		if target, isLink := fs.resolveLink(filename, f); isLink {
+			return fs.OpenFile(target, flag, perm)
+		}
+	}
+
+	if f.mode.IsDir() {
+		return nil, fmt.Errorf("cannot open directory: %s", filename)
+	}
+
+	return f.Duplicate(filename, perm, flag), nil
+}
+
+func (fs *Memory) resolveLink(fullpath string, f *file) (target string, isLink bool) {
+	if !isSymlink(f.mode) {
+		return fullpath, false
+	}
+
+	target = string(f.content.bytes)
+	if !isAbs(target) {
+		target = fs.Join(filepath.Dir(fullpath), target)
+	}
+
+	return target, true
+}
+
+// On Windows OS, IsAbs validates if a path is valid based on if stars with a
+// unit (eg.: `C:\`)  to assert that is absolute, but in this mem implementation
+// any path starting by `separator` is also considered absolute.
+func isAbs(path string) bool {
+	return filepath.IsAbs(path) || strings.HasPrefix(path, string(separator))
+}
+
+func (fs *Memory) Stat(filename string) (os.FileInfo, error) {
+	f, has := fs.s.Get(filename)
+	if !has {
+		return nil, os.ErrNotExist
+	}
+
+	fi, _ := f.Stat()
+
+	var err error
+	if target, isLink := fs.resolveLink(filename, f); isLink {
+		fi, err = fs.Stat(target)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	// the name of the file should always the name of the stated file, so we
+	// overwrite the Stat returned from the storage with it, since the
+	// filename may belong to a link.
+	fi.(*fileInfo).name = filepath.Base(filename)
+	return fi, nil
+}
+
+func (fs *Memory) Lstat(filename string) (os.FileInfo, error) {
+	f, has := fs.s.Get(filename)
+	if !has {
+		return nil, os.ErrNotExist
+	}
+
+	return f.Stat()
+}
+
+type ByName []os.FileInfo
+
+func (a ByName) Len() int           { return len(a) }
+func (a ByName) Less(i, j int) bool { return a[i].Name() < a[j].Name() }
+func (a ByName) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
+
+func (fs *Memory) ReadDir(path string) ([]os.FileInfo, error) {
+	if f, has := fs.s.Get(path); has {
+		if target, isLink := fs.resolveLink(path, f); isLink {
+			return fs.ReadDir(target)
+		}
+	} else {
+		return nil, &os.PathError{Op: "open", Path: path, Err: syscall.ENOENT}
+	}
+
+	var entries []os.FileInfo
+	for _, f := range fs.s.Children(path) {
+		fi, _ := f.Stat()
+		entries = append(entries, fi)
+	}
+
+	sort.Sort(ByName(entries))
+
+	return entries, nil
+}
+
+func (fs *Memory) MkdirAll(path string, perm os.FileMode) error {
+	_, err := fs.s.New(path, perm|os.ModeDir, 0)
+	return err
+}
+
+func (fs *Memory) TempFile(dir, prefix string) (billy.File, error) {
+	return util.TempFile(fs, dir, prefix)
+}
+
+func (fs *Memory) Rename(from, to string) error {
+	return fs.s.Rename(from, to)
+}
+
+func (fs *Memory) Remove(filename string) error {
+	return fs.s.Remove(filename)
+}
+
+func (fs *Memory) Join(elem ...string) string {
+	return filepath.Join(elem...)
+}
+
+func (fs *Memory) Symlink(target, link string) error {
+	_, err := fs.Stat(link)
+	if err == nil {
+		return os.ErrExist
+	}
+
+	if !os.IsNotExist(err) {
+		return err
+	}
+
+	return util.WriteFile(fs, link, []byte(target), 0777|os.ModeSymlink)
+}
+
+func (fs *Memory) Readlink(link string) (string, error) {
+	f, has := fs.s.Get(link)
+	if !has {
+		return "", os.ErrNotExist
+	}
+
+	if !isSymlink(f.mode) {
+		return "", &os.PathError{
+			Op:   "readlink",
+			Path: link,
+			Err:  fmt.Errorf("not a symlink"),
+		}
+	}
+
+	return string(f.content.bytes), nil
+}
+
+// Capabilities implements the Capable interface.
+func (fs *Memory) Capabilities() billy.Capability {
+	return billy.WriteCapability |
+		billy.ReadCapability |
+		billy.ReadAndWriteCapability |
+		billy.SeekCapability |
+		billy.TruncateCapability
+}
+
+type file struct {
+	name     string
+	content  *content
+	position int64
+	flag     int
+	mode     os.FileMode
+	mtime    time.Time
+
+	isClosed bool
+}
+
+func (f *file) Name() string {
+	return f.name
+}
+
+func (f *file) Read(b []byte) (int, error) {
+	n, err := f.ReadAt(b, f.position)
+	f.position += int64(n)
+
+	if err == io.EOF && n != 0 {
+		err = nil
+	}
+
+	return n, err
+}
+
+func (f *file) ReadAt(b []byte, off int64) (int, error) {
+	if f.isClosed {
+		return 0, os.ErrClosed
+	}
+
+	if !isReadAndWrite(f.flag) && !isReadOnly(f.flag) {
+		return 0, errors.New("read not supported")
+	}
+
+	n, err := f.content.ReadAt(b, off)
+
+	return n, err
+}
+
+func (f *file) Seek(offset int64, whence int) (int64, error) {
+	if f.isClosed {
+		return 0, os.ErrClosed
+	}
+
+	switch whence {
+	case io.SeekCurrent:
+		f.position += offset
+	case io.SeekStart:
+		f.position = offset
+	case io.SeekEnd:
+		f.position = int64(f.content.Len()) + offset
+	}
+
+	return f.position, nil
+}
+
+func (f *file) Write(p []byte) (int, error) {
+	return f.WriteAt(p, f.position)
+}
+
+func (f *file) WriteAt(p []byte, off int64) (int, error) {
+	if f.isClosed {
+		return 0, os.ErrClosed
+	}
+
+	if !isReadAndWrite(f.flag) && !isWriteOnly(f.flag) {
+		return 0, errors.New("write not supported")
+	}
+
+	n, err := f.content.WriteAt(p, off)
+	f.position = off + int64(n)
+	f.mtime = time.Now()
+
+	return n, err
+}
+
+func (f *file) Close() error {
+	if f.isClosed {
+		return os.ErrClosed
+	}
+
+	f.isClosed = true
+	return nil
+}
+
+func (f *file) Truncate(size int64) error {
+	if size < int64(len(f.content.bytes)) {
+		f.content.bytes = f.content.bytes[:size]
+	} else if more := int(size) - len(f.content.bytes); more > 0 {
+		f.content.bytes = append(f.content.bytes, make([]byte, more)...)
+	}
+	f.mtime = time.Now()
+
+	return nil
+}
+
+func (f *file) Duplicate(filename string, mode os.FileMode, flag int) billy.File {
+	new := &file{
+		name:    filename,
+		content: f.content,
+		mode:    mode,
+		flag:    flag,
+		mtime:   time.Now(),
+	}
+
+	if isTruncate(flag) {
+		new.content.Truncate()
+	}
+
+	if isAppend(flag) {
+		new.position = int64(new.content.Len())
+	}
+
+	return new
+}
+
+func (f *file) Stat() (os.FileInfo, error) {
+	return &fileInfo{
+		name:  f.Name(),
+		mode:  f.mode,
+		size:  f.content.Len(),
+		mtime: f.mtime,
+	}, nil
+}
+
+// Lock is a no-op in memfs.
+func (f *file) Lock() error {
+	return nil
+}
+
+// Unlock is a no-op in memfs.
+func (f *file) Unlock() error {
+	return nil
+}
+
+type fileInfo struct {
+	name  string
+	size  int
+	mode  os.FileMode
+	mtime time.Time
+}
+
+func (fi *fileInfo) Name() string {
+	return fi.name
+}
+
+func (fi *fileInfo) Size() int64 {
+	return int64(fi.size)
+}
+
+func (fi *fileInfo) Mode() os.FileMode {
+	return fi.mode
+}
+
+func (fi *fileInfo) ModTime() time.Time {
+	return fi.mtime
+}
+
+func (fi *fileInfo) IsDir() bool {
+	return fi.mode.IsDir()
+}
+
+func (*fileInfo) Sys() interface{} {
+	return nil
+}
+
+func (c *content) Truncate() {
+	c.bytes = make([]byte, 0)
+}
+
+func (c *content) Len() int {
+	return len(c.bytes)
+}
+
+func isCreate(flag int) bool {
+	return flag&os.O_CREATE != 0
+}
+
+func isExclusive(flag int) bool {
+	return flag&os.O_EXCL != 0
+}
+
+func isAppend(flag int) bool {
+	return flag&os.O_APPEND != 0
+}
+
+func isTruncate(flag int) bool {
+	return flag&os.O_TRUNC != 0
+}
+
+func isReadAndWrite(flag int) bool {
+	return flag&os.O_RDWR != 0
+}
+
+func isReadOnly(flag int) bool {
+	return flag == os.O_RDONLY
+}
+
+func isWriteOnly(flag int) bool {
+	return flag&os.O_WRONLY != 0
+}
+
+func isSymlink(m os.FileMode) bool {
+	return m&os.ModeSymlink != 0
+}
diff --git a/pkg/go-nfs/helpers/memfs/storage.go b/pkg/go-nfs/helpers/memfs/storage.go
new file mode 100644
index 0000000..5d73331
--- /dev/null
+++ b/pkg/go-nfs/helpers/memfs/storage.go
@@ -0,0 +1,243 @@
+package memfs
+
+import (
+	"errors"
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+	"strings"
+	"sync"
+	"time"
+)
+
+type storage struct {
+	files    map[string]*file
+	children map[string]map[string]*file
+}
+
+func newStorage() *storage {
+	return &storage{
+		files:    make(map[string]*file, 0),
+		children: make(map[string]map[string]*file, 0),
+	}
+}
+
+func (s *storage) Has(path string) bool {
+	path = clean(path)
+
+	_, ok := s.files[path]
+	return ok
+}
+
+func (s *storage) New(path string, mode os.FileMode, flag int) (*file, error) {
+	path = clean(path)
+	if s.Has(path) {
+		if !s.MustGet(path).mode.IsDir() {
+			return nil, fmt.Errorf("file already exists %q", path)
+		}
+
+		return nil, nil
+	}
+
+	name := filepath.Base(path)
+
+	f := &file{
+		name:    name,
+		content: &content{name: name},
+		mode:    mode,
+		flag:    flag,
+		mtime:   time.Now(),
+	}
+
+	s.files[path] = f
+	if err := s.createParent(path, mode, f); err != nil {
+		return nil, err
+	}
+	return f, nil
+}
+
+func (s *storage) createParent(path string, mode os.FileMode, f *file) error {
+	base := filepath.Dir(path)
+	base = clean(base)
+	if f.Name() == string(separator) {
+		return nil
+	}
+
+	if _, err := s.New(base, mode.Perm()|os.ModeDir, 0); err != nil {
+		return err
+	}
+
+	if _, ok := s.children[base]; !ok {
+		s.children[base] = make(map[string]*file, 0)
+	}
+
+	s.children[base][f.Name()] = f
+	return nil
+}
+
+func (s *storage) Children(path string) []*file {
+	path = clean(path)
+
+	l := make([]*file, 0)
+	for _, f := range s.children[path] {
+		l = append(l, f)
+	}
+
+	return l
+}
+
+func (s *storage) MustGet(path string) *file {
+	f, ok := s.Get(path)
+	if !ok {
+		panic(fmt.Errorf("couldn't find %q", path))
+	}
+
+	return f
+}
+
+func (s *storage) Get(path string) (*file, bool) {
+	path = clean(path)
+	if !s.Has(path) {
+		return nil, false
+	}
+
+	file, ok := s.files[path]
+	return file, ok
+}
+
+func (s *storage) Rename(from, to string) error {
+	from = clean(from)
+	to = clean(to)
+
+	if !s.Has(from) {
+		return os.ErrNotExist
+	}
+
+	move := [][2]string{{from, to}}
+
+	for pathFrom := range s.files {
+		if pathFrom == from || !strings.HasPrefix(pathFrom, from) {
+			continue
+		}
+
+		rel, _ := filepath.Rel(from, pathFrom)
+		pathTo := filepath.Join(to, rel)
+
+		move = append(move, [2]string{pathFrom, pathTo})
+	}
+
+	for _, ops := range move {
+		from := ops[0]
+		to := ops[1]
+
+		if err := s.move(from, to); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (s *storage) move(from, to string) error {
+	s.files[to] = s.files[from]
+	s.files[to].name = filepath.Base(to)
+	s.children[to] = s.children[from]
+
+	defer func() {
+		delete(s.children, from)
+		delete(s.files, from)
+		delete(s.children[filepath.Dir(from)], filepath.Base(from))
+	}()
+
+	return s.createParent(to, 0644, s.files[to])
+}
+
+func (s *storage) Remove(path string) error {
+	path = clean(path)
+
+	f, has := s.Get(path)
+	if !has {
+		return os.ErrNotExist
+	}
+
+	if f.mode.IsDir() && len(s.children[path]) != 0 {
+		return fmt.Errorf("dir: %s contains files", path)
+	}
+
+	base, file := filepath.Split(path)
+	base = filepath.Clean(base)
+
+	delete(s.children[base], file)
+	delete(s.files, path)
+	return nil
+}
+
+func clean(path string) string {
+	return filepath.Clean(filepath.FromSlash(path))
+}
+
+type content struct {
+	name  string
+	bytes []byte
+
+	m sync.RWMutex
+}
+
+func (c *content) WriteAt(p []byte, off int64) (int, error) {
+	if off < 0 {
+		return 0, &os.PathError{
+			Op:   "writeat",
+			Path: c.name,
+			Err:  errors.New("negative offset"),
+		}
+	}
+
+	c.m.Lock()
+	prev := len(c.bytes)
+
+	diff := int(off) - prev
+	if diff > 0 {
+		c.bytes = append(c.bytes, make([]byte, diff)...)
+	}
+
+	c.bytes = append(c.bytes[:off], p...)
+	if len(c.bytes) < prev {
+		c.bytes = c.bytes[:prev]
+	}
+	c.m.Unlock()
+
+	return len(p), nil
+}
+
+func (c *content) ReadAt(b []byte, off int64) (n int, err error) {
+	if off < 0 {
+		return 0, &os.PathError{
+			Op:   "readat",
+			Path: c.name,
+			Err:  errors.New("negative offset"),
+		}
+	}
+
+	c.m.RLock()
+	size := int64(len(c.bytes))
+	if off >= size {
+		c.m.RUnlock()
+		return 0, io.EOF
+	}
+
+	l := int64(len(b))
+	if off+l > size {
+		l = size - off
+	}
+
+	btr := c.bytes[off : off+l]
+	n = copy(b, btr)
+
+	if len(btr) < len(b) {
+		err = io.EOF
+	}
+	c.m.RUnlock()
+
+	return
+}
diff --git a/pkg/go-nfs/helpers/nullauthhandler.go b/pkg/go-nfs/helpers/nullauthhandler.go
new file mode 100644
index 0000000..87e4658
--- /dev/null
+++ b/pkg/go-nfs/helpers/nullauthhandler.go
@@ -0,0 +1,59 @@
+package helpers
+
+import (
+	"context"
+	"net"
+
+	"git.kmsign.ru/royalcat/tstor/pkg/ctxbilly"
+	nfs "git.kmsign.ru/royalcat/tstor/pkg/go-nfs"
+)
+
+// NewNullAuthHandler creates a handler for the provided filesystem
+func NewNullAuthHandler(fs nfs.Filesystem) nfs.Handler {
+	return &NullAuthHandler{fs}
+}
+
+// NullAuthHandler returns a NFS backing that exposes a given file system in response to all mount requests.
+type NullAuthHandler struct {
+	fs nfs.Filesystem
+}
+
+// Mount backs Mount RPC Requests, allowing for access control policies.
+func (h *NullAuthHandler) Mount(ctx context.Context, conn net.Conn, req nfs.MountRequest) (status nfs.MountStatus, hndl nfs.Filesystem, auths []nfs.AuthFlavor) {
+	status = nfs.MountStatusOk
+	hndl = h.fs
+	auths = []nfs.AuthFlavor{nfs.AuthFlavorNull}
+	return
+}
+
+// Change provides an interface for updating file attributes.
+func (h *NullAuthHandler) Change(fs nfs.Filesystem) nfs.Change {
+	if c, ok := h.fs.(ctxbilly.Change); ok {
+		return c
+	}
+	return nil
+}
+
+// FSStat provides information about a filesystem.
+func (h *NullAuthHandler) FSStat(ctx context.Context, f nfs.Filesystem, s *nfs.FSStat) error {
+	return nil
+}
+
+// ToHandle handled by CachingHandler
+func (h *NullAuthHandler) ToHandle(f nfs.Filesystem, s []string) []byte {
+	return []byte{}
+}
+
+// FromHandle handled by CachingHandler
+func (h *NullAuthHandler) FromHandle([]byte) (nfs.Filesystem, []string, error) {
+	return nil, []string{}, nil
+}
+
+func (c *NullAuthHandler) InvalidateHandle(nfs.Filesystem, []byte) error {
+	return nil
+}
+
+// HandleLImit handled by cachingHandler
+func (h *NullAuthHandler) HandleLimit() int {
+	return -1
+}
diff --git a/pkg/go-nfs/log.go b/pkg/go-nfs/log.go
new file mode 100644
index 0000000..db594d3
--- /dev/null
+++ b/pkg/go-nfs/log.go
@@ -0,0 +1,216 @@
+package nfs
+
+import (
+	"fmt"
+	"log"
+	"os"
+)
+
+var (
+	Log Logger = &DefaultLogger{}
+)
+
+type LogLevel int
+
+const (
+	PanicLevel LogLevel = iota
+	FatalLevel
+	ErrorLevel
+	WarnLevel
+	InfoLevel
+	DebugLevel
+	TraceLevel
+
+	panicLevelStr string = "[PANIC] "
+	fatalLevelStr string = "[FATAL] "
+	errorLevelStr string = "[ERROR] "
+	warnLevelStr  string = "[WARN] "
+	infoLevelStr  string = "[INFO] "
+	debugLevelStr string = "[DEBUG] "
+	traceLevelStr string = "[TRACE] "
+)
+
+type Logger interface {
+	SetLevel(level LogLevel)
+	GetLevel() LogLevel
+	ParseLevel(level string) (LogLevel, error)
+
+	Panic(args ...interface{})
+	Fatal(args ...interface{})
+	Error(args ...interface{})
+	Warn(args ...interface{})
+	Info(args ...interface{})
+	Debug(args ...interface{})
+	Trace(args ...interface{})
+	Print(args ...interface{})
+
+	Panicf(format string, args ...interface{})
+	Fatalf(format string, args ...interface{})
+	Errorf(format string, args ...interface{})
+	Warnf(format string, args ...interface{})
+	Infof(format string, args ...interface{})
+	Debugf(format string, args ...interface{})
+	Tracef(format string, args ...interface{})
+	Printf(format string, args ...interface{})
+}
+
+type DefaultLogger struct {
+	Level LogLevel
+}
+
+func SetLogger(logger Logger) {
+	Log = logger
+}
+
+func init() {
+	if os.Getenv("LOG_LEVEL") != "" {
+		if level, err := Log.ParseLevel(os.Getenv("LOG_LEVEL")); err == nil {
+			Log.SetLevel(level)
+		}
+	} else {
+		// set default log level to info
+		Log.SetLevel(InfoLevel)
+	}
+}
+
+func (l *DefaultLogger) GetLevel() LogLevel {
+	return l.Level
+}
+
+func (l *DefaultLogger) SetLevel(level LogLevel) {
+	l.Level = level
+}
+
+func (l *DefaultLogger) ParseLevel(level string) (LogLevel, error) {
+	switch level {
+	case "panic":
+		return PanicLevel, nil
+	case "fatal":
+		return FatalLevel, nil
+	case "error":
+		return ErrorLevel, nil
+	case "warn":
+		return WarnLevel, nil
+	case "info":
+		return InfoLevel, nil
+	case "debug":
+		return DebugLevel, nil
+	case "trace":
+		return TraceLevel, nil
+	}
+	var ll LogLevel
+	return ll, fmt.Errorf("invalid log level %q", level)
+}
+
+func (l *DefaultLogger) Panic(args ...interface{}) {
+	if l.Level < PanicLevel {
+		return
+	}
+	args = append([]interface{}{panicLevelStr}, args...)
+	log.Print(args...)
+}
+
+func (l *DefaultLogger) Panicf(format string, args ...interface{}) {
+	if l.Level < PanicLevel {
+		return
+	}
+	log.Printf(panicLevelStr+format, args...)
+}
+
+func (l *DefaultLogger) Fatal(args ...interface{}) {
+	if l.Level < FatalLevel {
+		return
+	}
+	args = append([]interface{}{fatalLevelStr}, args...)
+	log.Print(args...)
+}
+
+func (l *DefaultLogger) Fatalf(format string, args ...interface{}) {
+	if l.Level < FatalLevel {
+		return
+	}
+	log.Printf(fatalLevelStr+format, args...)
+}
+
+func (l *DefaultLogger) Error(args ...interface{}) {
+	if l.Level < ErrorLevel {
+		return
+	}
+	args = append([]interface{}{errorLevelStr}, args...)
+	log.Print(args...)
+}
+
+func (l *DefaultLogger) Errorf(format string, args ...interface{}) {
+	if l.Level < ErrorLevel {
+		return
+	}
+	log.Printf(errorLevelStr+format, args...)
+}
+
+func (l *DefaultLogger) Warn(args ...interface{}) {
+	if l.Level < WarnLevel {
+		return
+	}
+	args = append([]interface{}{warnLevelStr}, args...)
+	log.Print(args...)
+}
+
+func (l *DefaultLogger) Warnf(format string, args ...interface{}) {
+	if l.Level < WarnLevel {
+		return
+	}
+	log.Printf(warnLevelStr+format, args...)
+}
+
+func (l *DefaultLogger) Info(args ...interface{}) {
+	if l.Level < InfoLevel {
+		return
+	}
+	args = append([]interface{}{infoLevelStr}, args...)
+	log.Print(args...)
+}
+
+func (l *DefaultLogger) Infof(format string, args ...interface{}) {
+	if l.Level < InfoLevel {
+		return
+	}
+	log.Printf(infoLevelStr+format, args...)
+}
+
+func (l *DefaultLogger) Debug(args ...interface{}) {
+	if l.Level < DebugLevel {
+		return
+	}
+	args = append([]interface{}{debugLevelStr}, args...)
+	log.Print(args...)
+}
+
+func (l *DefaultLogger) Debugf(format string, args ...interface{}) {
+	if l.Level < DebugLevel {
+		return
+	}
+	log.Printf(debugLevelStr+format, args...)
+}
+
+func (l *DefaultLogger) Trace(args ...interface{}) {
+	if l.Level < TraceLevel {
+		return
+	}
+	args = append([]interface{}{traceLevelStr}, args...)
+	log.Print(args...)
+}
+
+func (l *DefaultLogger) Tracef(format string, args ...interface{}) {
+	if l.Level < TraceLevel {
+		return
+	}
+	log.Printf(traceLevelStr+format, args...)
+}
+
+func (l *DefaultLogger) Print(args ...interface{}) {
+	log.Print(args...)
+}
+
+func (l *DefaultLogger) Printf(format string, args ...interface{}) {
+	log.Printf(format, args...)
+}
diff --git a/pkg/go-nfs/mount.go b/pkg/go-nfs/mount.go
new file mode 100644
index 0000000..e95d098
--- /dev/null
+++ b/pkg/go-nfs/mount.go
@@ -0,0 +1,58 @@
+package nfs
+
+import (
+	"bytes"
+	"context"
+
+	"github.com/willscott/go-nfs-client/nfs/xdr"
+)
+
+const (
+	mountServiceID = 100005
+)
+
+func init() {
+	_ = RegisterMessageHandler(mountServiceID, uint32(MountProcNull), onMountNull)
+	_ = RegisterMessageHandler(mountServiceID, uint32(MountProcMount), onMount)
+	_ = RegisterMessageHandler(mountServiceID, uint32(MountProcUmnt), onUMount)
+}
+
+func onMountNull(ctx context.Context, w *response, userHandle Handler) error {
+	return w.writeHeader(ResponseCodeSuccess)
+}
+
+func onMount(ctx context.Context, w *response, userHandle Handler) error {
+	// TODO: auth check.
+	dirpath, err := xdr.ReadOpaque(w.req.Body)
+	if err != nil {
+		return err
+	}
+	mountReq := MountRequest{Header: w.req.Header, Dirpath: dirpath}
+	status, handle, flavors := userHandle.Mount(ctx, w.conn, mountReq)
+
+	if err := w.writeHeader(ResponseCodeSuccess); err != nil {
+		return err
+	}
+
+	writer := bytes.NewBuffer([]byte{})
+	if err := xdr.Write(writer, uint32(status)); err != nil {
+		return err
+	}
+
+	rootHndl := userHandle.ToHandle(handle, []string{})
+
+	if status == MountStatusOk {
+		_ = xdr.Write(writer, rootHndl)
+		_ = xdr.Write(writer, flavors)
+	}
+	return w.Write(writer.Bytes())
+}
+
+func onUMount(ctx context.Context, w *response, userHandle Handler) error {
+	_, err := xdr.ReadOpaque(w.req.Body)
+	if err != nil {
+		return err
+	}
+
+	return w.writeHeader(ResponseCodeSuccess)
+}
diff --git a/pkg/go-nfs/mountinterface.go b/pkg/go-nfs/mountinterface.go
new file mode 100644
index 0000000..1dc39ee
--- /dev/null
+++ b/pkg/go-nfs/mountinterface.go
@@ -0,0 +1,90 @@
+package nfs
+
+import (
+	"github.com/willscott/go-nfs-client/nfs/rpc"
+)
+
+// FHSize is the maximum size of a FileHandle
+const FHSize = 64
+
+// MNTNameLen is the maximum size of a mount name
+const MNTNameLen = 255
+
+// MntPathLen is the maximum size of a mount path
+const MntPathLen = 1024
+
+// FileHandle maps to a fhandle3
+type FileHandle []byte
+
+// MountStatus defines the response to the Mount Procedure
+type MountStatus uint32
+
+// MountStatus Codes
+const (
+	MountStatusOk             MountStatus = 0
+	MountStatusErrPerm        MountStatus = 1
+	MountStatusErrNoEnt       MountStatus = 2
+	MountStatusErrIO          MountStatus = 5
+	MountStatusErrAcces       MountStatus = 13
+	MountStatusErrNotDir      MountStatus = 20
+	MountStatusErrInval       MountStatus = 22
+	MountStatusErrNameTooLong MountStatus = 63
+	MountStatusErrNotSupp     MountStatus = 10004
+	MountStatusErrServerFault MountStatus = 10006
+)
+
+// MountProcedure is the valid RPC calls for the mount service.
+type MountProcedure uint32
+
+// MountProcedure Codes
+const (
+	MountProcNull MountProcedure = iota
+	MountProcMount
+	MountProcDump
+	MountProcUmnt
+	MountProcUmntAll
+	MountProcExport
+)
+
+func (m MountProcedure) String() string {
+	switch m {
+	case MountProcNull:
+		return "Null"
+	case MountProcMount:
+		return "Mount"
+	case MountProcDump:
+		return "Dump"
+	case MountProcUmnt:
+		return "Umnt"
+	case MountProcUmntAll:
+		return "UmntAll"
+	case MountProcExport:
+		return "Export"
+	default:
+		return "Unknown"
+	}
+}
+
+// AuthFlavor is a form of authentication, per rfc1057 section 7.2
+type AuthFlavor uint32
+
+// AuthFlavor Codes
+const (
+	AuthFlavorNull  AuthFlavor = 0
+	AuthFlavorUnix  AuthFlavor = 1
+	AuthFlavorShort AuthFlavor = 2
+	AuthFlavorDES   AuthFlavor = 3
+)
+
+// MountRequest contains the format of a client request to open a mount.
+type MountRequest struct {
+	rpc.Header
+	Dirpath []byte
+}
+
+// MountResponse is the server's response with status `MountStatusOk`
+type MountResponse struct {
+	rpc.Header
+	FileHandle
+	AuthFlavors []int
+}
diff --git a/pkg/go-nfs/nfs.go b/pkg/go-nfs/nfs.go
new file mode 100644
index 0000000..bf85e0a
--- /dev/null
+++ b/pkg/go-nfs/nfs.go
@@ -0,0 +1,38 @@
+package nfs
+
+import (
+	"context"
+)
+
+const (
+	nfsServiceID = 100003
+)
+
+func init() {
+	_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureNull), onNull)               // 0
+	_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureGetAttr), onGetAttr)         // 1
+	_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureSetAttr), onSetAttr)         // 2
+	_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureLookup), onLookup)           // 3
+	_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureAccess), onAccess)           // 4
+	_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureReadlink), onReadLink)       // 5
+	_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureRead), onRead)               // 6
+	_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureWrite), onWrite)             // 7
+	_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureCreate), onCreate)           // 8
+	_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureMkDir), onMkdir)             // 9
+	_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureSymlink), onSymlink)         // 10
+	_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureMkNod), onMknod)             // 11
+	_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureRemove), onRemove)           // 12
+	_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureRmDir), onRmDir)             // 13
+	_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureRename), onRename)           // 14
+	_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureLink), onLink)               // 15
+	_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureReadDir), onReadDir)         // 16
+	_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureReadDirPlus), onReadDirPlus) // 17
+	_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureFSStat), onFSStat)           // 18
+	_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureFSInfo), onFSInfo)           // 19
+	_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedurePathConf), onPathConf)       // 20
+	_ = RegisterMessageHandler(nfsServiceID, uint32(NFSProcedureCommit), onCommit)           // 21
+}
+
+func onNull(ctx context.Context, w *response, userHandle Handler) error {
+	return w.Write([]byte{})
+}
diff --git a/pkg/go-nfs/nfs_onaccess.go b/pkg/go-nfs/nfs_onaccess.go
new file mode 100644
index 0000000..6674734
--- /dev/null
+++ b/pkg/go-nfs/nfs_onaccess.go
@@ -0,0 +1,45 @@
+package nfs
+
+import (
+	"bytes"
+	"context"
+
+	billy "github.com/go-git/go-billy/v5"
+	"github.com/willscott/go-nfs-client/nfs/xdr"
+)
+
+func onAccess(ctx context.Context, w *response, userHandle Handler) error {
+	w.errorFmt = opAttrErrorFormatter
+	roothandle, err := xdr.ReadOpaque(w.req.Body)
+	if err != nil {
+		return &NFSStatusError{NFSStatusInval, err}
+	}
+	fs, path, err := userHandle.FromHandle(roothandle)
+	if err != nil {
+		return &NFSStatusError{NFSStatusStale, err}
+	}
+	mask, err := xdr.ReadUint32(w.req.Body)
+	if err != nil {
+		return &NFSStatusError{NFSStatusInval, err}
+	}
+
+	writer := bytes.NewBuffer([]byte{})
+	if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if err := WritePostOpAttrs(writer, tryStat(ctx, fs, path)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	if !CapabilityCheck(fs, billy.WriteCapability) {
+		mask = mask & (1 | 2 | 0x20)
+	}
+
+	if err := xdr.Write(writer, mask); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if err := w.Write(writer.Bytes()); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	return nil
+}
diff --git a/pkg/go-nfs/nfs_oncommit.go b/pkg/go-nfs/nfs_oncommit.go
new file mode 100644
index 0000000..e2616d6
--- /dev/null
+++ b/pkg/go-nfs/nfs_oncommit.go
@@ -0,0 +1,51 @@
+package nfs
+
+import (
+	"bytes"
+	"context"
+	"os"
+
+	billy "github.com/go-git/go-billy/v5"
+	"github.com/willscott/go-nfs-client/nfs/xdr"
+)
+
+// onCommit - note this is a no-op, as we always push writes to the backing store.
+func onCommit(ctx context.Context, w *response, userHandle Handler) error {
+	w.errorFmt = wccDataErrorFormatter
+	handle, err := xdr.ReadOpaque(w.req.Body)
+	if err != nil {
+		return &NFSStatusError{NFSStatusInval, err}
+	}
+	// The conn will drain the unread offset and count arguments.
+
+	fs, path, err := userHandle.FromHandle(handle)
+	if err != nil {
+		return &NFSStatusError{NFSStatusStale, err}
+	}
+
+	if !CapabilityCheck(fs, billy.WriteCapability) {
+		return &NFSStatusError{NFSStatusServerFault, os.ErrPermission}
+	}
+
+	writer := bytes.NewBuffer([]byte{})
+	if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
+		return err
+	}
+
+	// no pre-op cache data.
+	if err := xdr.Write(writer, uint32(0)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if err := WritePostOpAttrs(writer, tryStat(ctx, fs, path)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	// write the 8 bytes of write verification.
+	if err := xdr.Write(writer, w.Server.ID); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	if err := w.Write(writer.Bytes()); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	return nil
+}
diff --git a/pkg/go-nfs/nfs_oncreate.go b/pkg/go-nfs/nfs_oncreate.go
new file mode 100644
index 0000000..3a181d0
--- /dev/null
+++ b/pkg/go-nfs/nfs_oncreate.go
@@ -0,0 +1,125 @@
+package nfs
+
+import (
+	"bytes"
+	"context"
+	"os"
+
+	billy "github.com/go-git/go-billy/v5"
+	"github.com/willscott/go-nfs-client/nfs/xdr"
+)
+
+const (
+	createModeUnchecked = 0
+	createModeGuarded   = 1
+	createModeExclusive = 2
+)
+
+func onCreate(ctx context.Context, w *response, userHandle Handler) error {
+	w.errorFmt = wccDataErrorFormatter
+	obj := DirOpArg{}
+	err := xdr.Read(w.req.Body, &obj)
+	if err != nil {
+		return &NFSStatusError{NFSStatusInval, err}
+	}
+	how, err := xdr.ReadUint32(w.req.Body)
+	if err != nil {
+		return &NFSStatusError{NFSStatusInval, err}
+	}
+	var attrs *SetFileAttributes
+	if how == createModeUnchecked || how == createModeGuarded {
+		sattr, err := ReadSetFileAttributes(w.req.Body)
+		if err != nil {
+			return &NFSStatusError{NFSStatusInval, err}
+		}
+		attrs = sattr
+	} else if how == createModeExclusive {
+		// read createverf3
+		var verf [8]byte
+		if err := xdr.Read(w.req.Body, &verf); err != nil {
+			return &NFSStatusError{NFSStatusInval, err}
+		}
+		Log.Errorf("failing create to indicate lack of support for 'exclusive' mode.")
+		// TODO: support 'exclusive' mode.
+		return &NFSStatusError{NFSStatusNotSupp, os.ErrPermission}
+	} else {
+		// invalid
+		return &NFSStatusError{NFSStatusNotSupp, os.ErrInvalid}
+	}
+
+	fs, path, err := userHandle.FromHandle(obj.Handle)
+	if err != nil {
+		return &NFSStatusError{NFSStatusStale, err}
+	}
+
+	if !CapabilityCheck(fs, billy.WriteCapability) {
+		return &NFSStatusError{NFSStatusROFS, os.ErrPermission}
+	}
+
+	if len(string(obj.Filename)) > PathNameMax {
+		return &NFSStatusError{NFSStatusNameTooLong, nil}
+	}
+
+	newFile := append(path, string(obj.Filename))
+	newFilePath := fs.Join(newFile...)
+	if s, err := fs.Stat(ctx, newFilePath); err == nil {
+		if s.IsDir() {
+			return &NFSStatusError{NFSStatusExist, nil}
+		}
+		if how == createModeGuarded {
+			return &NFSStatusError{NFSStatusExist, os.ErrPermission}
+		}
+	} else {
+		if s, err := fs.Stat(ctx, fs.Join(path...)); err != nil {
+			return &NFSStatusError{NFSStatusAccess, err}
+		} else if !s.IsDir() {
+			return &NFSStatusError{NFSStatusNotDir, nil}
+		}
+	}
+
+	file, err := fs.Create(ctx, newFilePath)
+	if err != nil {
+		Log.Errorf("Error Creating: %v", err)
+		return &NFSStatusError{NFSStatusAccess, err}
+	}
+	if err := file.Close(ctx); err != nil {
+		Log.Errorf("Error Creating: %v", err)
+		return &NFSStatusError{NFSStatusAccess, err}
+	}
+
+	fp := userHandle.ToHandle(fs, newFile)
+	changer := userHandle.Change(fs)
+	if err := attrs.Apply(ctx, changer, fs, newFilePath); err != nil {
+		Log.Errorf("Error applying attributes: %v\n", err)
+		return &NFSStatusError{NFSStatusIO, err}
+	}
+
+	writer := bytes.NewBuffer([]byte{})
+	if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	// "handle follows"
+	if err := xdr.Write(writer, uint32(1)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if err := xdr.Write(writer, fp); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if err := WritePostOpAttrs(writer, tryStat(ctx, fs, []string{file.Name()})); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	// dir_wcc (we don't include pre_op_attr)
+	if err := xdr.Write(writer, uint32(0)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if err := WritePostOpAttrs(writer, tryStat(ctx, fs, path)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	if err := w.Write(writer.Bytes()); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	return nil
+}
diff --git a/pkg/go-nfs/nfs_onfsinfo.go b/pkg/go-nfs/nfs_onfsinfo.go
new file mode 100644
index 0000000..152e366
--- /dev/null
+++ b/pkg/go-nfs/nfs_onfsinfo.go
@@ -0,0 +1,89 @@
+package nfs
+
+import (
+	"bytes"
+	"context"
+
+	"github.com/go-git/go-billy/v5"
+	"github.com/willscott/go-nfs-client/nfs/xdr"
+)
+
+const (
+	// FSInfoPropertyLink does the FS support hard links?
+	FSInfoPropertyLink = 0x0001
+	// FSInfoPropertySymlink does the FS support soft links?
+	FSInfoPropertySymlink = 0x0002
+	// FSInfoPropertyHomogeneous does the FS need PATHCONF calls for each file
+	FSInfoPropertyHomogeneous = 0x0008
+	// FSInfoPropertyCanSetTime can the FS support setting access/mod times?
+	FSInfoPropertyCanSetTime = 0x0010
+)
+
+func onFSInfo(ctx context.Context, w *response, userHandle Handler) error {
+	roothandle, err := xdr.ReadOpaque(w.req.Body)
+	if err != nil {
+		return &NFSStatusError{NFSStatusInval, err}
+	}
+	fs, path, err := userHandle.FromHandle(roothandle)
+	if err != nil {
+		return &NFSStatusError{NFSStatusStale, err}
+	}
+
+	writer := bytes.NewBuffer([]byte{})
+	if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if err := WritePostOpAttrs(writer, tryStat(ctx, fs, path)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	type fsinfores struct {
+		Rtmax       uint32
+		Rtpref      uint32
+		Rtmult      uint32
+		Wtmax       uint32
+		Wtpref      uint32
+		Wtmult      uint32
+		Dtpref      uint32
+		Maxfilesize uint64
+		TimeDelta   uint64
+		Properties  uint32
+	}
+
+	res := fsinfores{
+		Rtmax:       1 << 30,
+		Rtpref:      1 << 30,
+		Rtmult:      4096,
+		Wtmax:       1 << 30,
+		Wtpref:      1 << 30,
+		Wtmult:      4096,
+		Dtpref:      8192,
+		Maxfilesize: 1 << 62, // wild guess. this seems big.
+		TimeDelta:   1,       // nanosecond precision.
+		Properties:  0,
+	}
+
+	// TODO: these aren't great indications of support, really.
+	// if _, ok := fs.(billy.Symlink); ok {
+	// 	res.Properties |= FSInfoPropertyLink
+	// 	res.Properties |= FSInfoPropertySymlink
+	// }
+	// TODO: if the nfs share spans multiple virtual mounts, may need
+	// to support granular PATHINFO responses.
+	res.Properties |= FSInfoPropertyHomogeneous
+	// TODO: not a perfect indicator
+
+	if CapabilityCheck(fs, billy.WriteCapability) {
+		res.Properties |= FSInfoPropertyCanSetTime
+	}
+
+	// TODO: this whole struct should be specifiable by the userhandler.
+
+	if err := xdr.Write(writer, res); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if err := w.Write(writer.Bytes()); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	return nil
+}
diff --git a/pkg/go-nfs/nfs_onfsstat.go b/pkg/go-nfs/nfs_onfsstat.go
new file mode 100644
index 0000000..325a106
--- /dev/null
+++ b/pkg/go-nfs/nfs_onfsstat.go
@@ -0,0 +1,59 @@
+package nfs
+
+import (
+	"bytes"
+	"context"
+
+	billy "github.com/go-git/go-billy/v5"
+	"github.com/willscott/go-nfs-client/nfs/xdr"
+)
+
+func onFSStat(ctx context.Context, w *response, userHandle Handler) error {
+	roothandle, err := xdr.ReadOpaque(w.req.Body)
+	if err != nil {
+		return &NFSStatusError{NFSStatusInval, err}
+	}
+	fs, path, err := userHandle.FromHandle(roothandle)
+	if err != nil {
+		return &NFSStatusError{NFSStatusStale, err}
+	}
+
+	defaults := FSStat{
+		TotalSize:      1 << 62,
+		FreeSize:       1 << 62,
+		AvailableSize:  1 << 62,
+		TotalFiles:     1 << 62,
+		FreeFiles:      1 << 62,
+		AvailableFiles: 1 << 62,
+		CacheHint:      0,
+	}
+
+	if !CapabilityCheck(fs, billy.WriteCapability) {
+		defaults.AvailableFiles = 0
+		defaults.AvailableSize = 0
+	}
+
+	err = userHandle.FSStat(ctx, fs, &defaults)
+	if err != nil {
+		if _, ok := err.(*NFSStatusError); ok {
+			return err
+		}
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	writer := bytes.NewBuffer([]byte{})
+	if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if err := WritePostOpAttrs(writer, tryStat(ctx, fs, path)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	if err := xdr.Write(writer, defaults); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if err := w.Write(writer.Bytes()); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	return nil
+}
diff --git a/pkg/go-nfs/nfs_ongetattr.go b/pkg/go-nfs/nfs_ongetattr.go
new file mode 100644
index 0000000..40c4148
--- /dev/null
+++ b/pkg/go-nfs/nfs_ongetattr.go
@@ -0,0 +1,48 @@
+package nfs
+
+import (
+	"bytes"
+	"context"
+	"errors"
+	"os"
+
+	"github.com/willscott/go-nfs-client/nfs/xdr"
+)
+
+func onGetAttr(ctx context.Context, w *response, userHandle Handler) error {
+	handle, err := xdr.ReadOpaque(w.req.Body)
+	if err != nil {
+		return &NFSStatusError{NFSStatusInval, err}
+	}
+
+	fs, path, err := userHandle.FromHandle(handle)
+	if err != nil {
+		return &NFSStatusError{NFSStatusStale, err}
+	}
+
+	fullPath := fs.Join(path...)
+	info, err := fs.Lstat(ctx, fullPath)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return &NFSStatusError{NFSStatusNoEnt, err}
+		}
+		if errors.Is(err, context.DeadlineExceeded) {
+			return &NFSStatusError{NFSStatusJukebox, err}
+		}
+		return &NFSStatusError{NFSStatusIO, err}
+	}
+	attr := ToFileAttribute(info, fullPath)
+
+	writer := bytes.NewBuffer([]byte{})
+	if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if err := xdr.Write(writer, attr); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	if err := w.Write(writer.Bytes()); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	return nil
+}
diff --git a/pkg/go-nfs/nfs_onlink.go b/pkg/go-nfs/nfs_onlink.go
new file mode 100644
index 0000000..460a969
--- /dev/null
+++ b/pkg/go-nfs/nfs_onlink.go
@@ -0,0 +1,94 @@
+package nfs
+
+import (
+	"bytes"
+	"context"
+	"os"
+
+	"github.com/go-git/go-billy/v5"
+	"github.com/willscott/go-nfs-client/nfs/xdr"
+)
+
+// Backing billy.FS doesn't support hard links
+func onLink(ctx context.Context, w *response, userHandle Handler) error {
+	w.errorFmt = wccDataErrorFormatter
+	obj := DirOpArg{}
+	err := xdr.Read(w.req.Body, &obj)
+	if err != nil {
+		return &NFSStatusError{NFSStatusInval, err}
+	}
+	attrs, err := ReadSetFileAttributes(w.req.Body)
+	if err != nil {
+		return &NFSStatusError{NFSStatusInval, err}
+	}
+
+	target, err := xdr.ReadOpaque(w.req.Body)
+	if err != nil {
+		return &NFSStatusError{NFSStatusInval, err}
+	}
+
+	fs, path, err := userHandle.FromHandle(obj.Handle)
+	if err != nil {
+		return &NFSStatusError{NFSStatusStale, err}
+	}
+	if !CapabilityCheck(fs, billy.WriteCapability) {
+		return &NFSStatusError{NFSStatusROFS, os.ErrPermission}
+	}
+
+	if len(string(obj.Filename)) > PathNameMax {
+		return &NFSStatusError{NFSStatusNameTooLong, os.ErrInvalid}
+	}
+
+	newFilePath := fs.Join(append(path, string(obj.Filename))...)
+	if _, err := fs.Stat(ctx, newFilePath); err == nil {
+		return &NFSStatusError{NFSStatusExist, os.ErrExist}
+	}
+	if s, err := fs.Stat(ctx, fs.Join(path...)); err != nil {
+		return &NFSStatusError{NFSStatusAccess, err}
+	} else if !s.IsDir() {
+		return &NFSStatusError{NFSStatusNotDir, nil}
+	}
+
+	fp := userHandle.ToHandle(fs, append(path, string(obj.Filename)))
+	changer := userHandle.Change(fs)
+	if changer == nil {
+		return &NFSStatusError{NFSStatusAccess, err}
+	}
+	cos, ok := changer.(UnixChange)
+	if !ok {
+		return &NFSStatusError{NFSStatusAccess, err}
+	}
+
+	err = cos.Link(ctx, string(target), newFilePath)
+	if err != nil {
+		return &NFSStatusError{NFSStatusAccess, err}
+	}
+	if err := attrs.Apply(ctx, changer, fs, newFilePath); err != nil {
+		return &NFSStatusError{NFSStatusIO, err}
+	}
+
+	writer := bytes.NewBuffer([]byte{})
+	if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	// "handle follows"
+	if err := xdr.Write(writer, uint32(1)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if err := xdr.Write(writer, fp); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if err := WritePostOpAttrs(writer, tryStat(ctx, fs, append(path, string(obj.Filename)))); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	if err := WriteWcc(writer, nil, tryStat(ctx, fs, path)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	if err := w.Write(writer.Bytes()); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	return nil
+}
diff --git a/pkg/go-nfs/nfs_onlookup.go b/pkg/go-nfs/nfs_onlookup.go
new file mode 100644
index 0000000..6507d03
--- /dev/null
+++ b/pkg/go-nfs/nfs_onlookup.go
@@ -0,0 +1,86 @@
+package nfs
+
+import (
+	"bytes"
+	"context"
+	"os"
+
+	"github.com/willscott/go-nfs-client/nfs/xdr"
+)
+
+func lookupSuccessResponse(ctx context.Context, handle []byte, entPath, dirPath []string, fs Filesystem) ([]byte, error) {
+	writer := bytes.NewBuffer([]byte{})
+	if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
+		return nil, err
+	}
+	if err := xdr.Write(writer, handle); err != nil {
+		return nil, err
+	}
+	if err := WritePostOpAttrs(writer, tryStat(ctx, fs, entPath)); err != nil {
+		return nil, err
+	}
+	if err := WritePostOpAttrs(writer, tryStat(ctx, fs, dirPath)); err != nil {
+		return nil, err
+	}
+	return writer.Bytes(), nil
+}
+
+func onLookup(ctx context.Context, w *response, userHandle Handler) error {
+	w.errorFmt = opAttrErrorFormatter
+	obj := DirOpArg{}
+	err := xdr.Read(w.req.Body, &obj)
+	if err != nil {
+		return &NFSStatusError{NFSStatusInval, err}
+	}
+
+	fs, p, err := userHandle.FromHandle(obj.Handle)
+	if err != nil {
+		return &NFSStatusError{NFSStatusStale, err}
+	}
+	dirInfo, err := fs.Lstat(ctx, fs.Join(p...))
+	if err != nil || !dirInfo.IsDir() {
+		return &NFSStatusError{NFSStatusNotDir, err}
+	}
+
+	// Special cases for "." and ".."
+	if bytes.Equal(obj.Filename, []byte(".")) {
+		resp, err := lookupSuccessResponse(ctx, obj.Handle, p, p, fs)
+		if err != nil {
+			return &NFSStatusError{NFSStatusServerFault, err}
+		}
+		if err := w.Write(resp); err != nil {
+			return &NFSStatusError{NFSStatusServerFault, err}
+		}
+		return nil
+	}
+	if bytes.Equal(obj.Filename, []byte("..")) {
+		if len(p) == 0 {
+			return &NFSStatusError{NFSStatusAccess, os.ErrPermission}
+		}
+		pPath := p[0 : len(p)-1]
+		pHandle := userHandle.ToHandle(fs, pPath)
+		resp, err := lookupSuccessResponse(ctx, pHandle, pPath, p, fs)
+		if err != nil {
+			return &NFSStatusError{NFSStatusServerFault, err}
+		}
+		if err := w.Write(resp); err != nil {
+			return &NFSStatusError{NFSStatusServerFault, err}
+		}
+		return nil
+	}
+
+	reqPath := append(p, string(obj.Filename))
+	if _, err = fs.Lstat(ctx, fs.Join(reqPath...)); err != nil {
+		return &NFSStatusError{NFSStatusNoEnt, os.ErrNotExist}
+	}
+
+	newHandle := userHandle.ToHandle(fs, reqPath)
+	resp, err := lookupSuccessResponse(ctx, newHandle, reqPath, p, fs)
+	if err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if err := w.Write(resp); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	return nil
+}
diff --git a/pkg/go-nfs/nfs_onmkdir.go b/pkg/go-nfs/nfs_onmkdir.go
new file mode 100644
index 0000000..d96bab1
--- /dev/null
+++ b/pkg/go-nfs/nfs_onmkdir.go
@@ -0,0 +1,94 @@
+package nfs
+
+import (
+	"bytes"
+	"context"
+	"os"
+
+	"github.com/go-git/go-billy/v5"
+	"github.com/willscott/go-nfs-client/nfs/xdr"
+)
+
+const (
+	mkdirDefaultMode = 755
+)
+
+func onMkdir(ctx context.Context, w *response, userHandle Handler) error {
+	w.errorFmt = wccDataErrorFormatter
+	obj := DirOpArg{}
+	err := xdr.Read(w.req.Body, &obj)
+	if err != nil {
+		return &NFSStatusError{NFSStatusInval, err}
+	}
+
+	attrs, err := ReadSetFileAttributes(w.req.Body)
+	if err != nil {
+		return &NFSStatusError{NFSStatusInval, err}
+	}
+
+	fs, path, err := userHandle.FromHandle(obj.Handle)
+	if err != nil {
+		return &NFSStatusError{NFSStatusStale, err}
+	}
+	if !CapabilityCheck(fs, billy.WriteCapability) {
+		return &NFSStatusError{NFSStatusROFS, os.ErrPermission}
+	}
+
+	if len(string(obj.Filename)) > PathNameMax {
+		return &NFSStatusError{NFSStatusNameTooLong, os.ErrInvalid}
+	}
+	if string(obj.Filename) == "." || string(obj.Filename) == ".." {
+		return &NFSStatusError{NFSStatusExist, os.ErrExist}
+	}
+
+	newFolder := append(path, string(obj.Filename))
+	newFolderPath := fs.Join(newFolder...)
+	if s, err := fs.Stat(ctx, newFolderPath); err == nil {
+		if s.IsDir() {
+			return &NFSStatusError{NFSStatusExist, nil}
+		}
+	} else {
+		if s, err := fs.Stat(ctx, fs.Join(path...)); err != nil {
+			return &NFSStatusError{NFSStatusAccess, err}
+		} else if !s.IsDir() {
+			return &NFSStatusError{NFSStatusNotDir, nil}
+		}
+	}
+
+	if err := fs.MkdirAll(ctx, newFolderPath, attrs.Mode(mkdirDefaultMode)); err != nil {
+		return &NFSStatusError{NFSStatusAccess, err}
+	}
+
+	fp := userHandle.ToHandle(fs, newFolder)
+	changer := userHandle.Change(fs)
+	if changer != nil {
+		if err := attrs.Apply(ctx, changer, fs, newFolderPath); err != nil {
+			return &NFSStatusError{NFSStatusIO, err}
+		}
+	}
+
+	writer := bytes.NewBuffer([]byte{})
+	if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	// "handle follows"
+	if err := xdr.Write(writer, uint32(1)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if err := xdr.Write(writer, fp); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if err := WritePostOpAttrs(writer, tryStat(ctx, fs, newFolder)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	if err := WriteWcc(writer, nil, tryStat(ctx, fs, path)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	if err := w.Write(writer.Bytes()); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	return nil
+}
diff --git a/pkg/go-nfs/nfs_onmknod.go b/pkg/go-nfs/nfs_onmknod.go
new file mode 100644
index 0000000..81ca2fa
--- /dev/null
+++ b/pkg/go-nfs/nfs_onmknod.go
@@ -0,0 +1,158 @@
+package nfs
+
+import (
+	"bytes"
+	"context"
+	"os"
+
+	billy "github.com/go-git/go-billy/v5"
+	"github.com/willscott/go-nfs-client/nfs/xdr"
+)
+
+type nfs_ftype int32
+
+const (
+	FTYPE_NF3REG  nfs_ftype = 1
+	FTYPE_NF3DIR  nfs_ftype = 2
+	FTYPE_NF3BLK  nfs_ftype = 3
+	FTYPE_NF3CHR  nfs_ftype = 4
+	FTYPE_NF3LNK  nfs_ftype = 5
+	FTYPE_NF3SOCK nfs_ftype = 6
+	FTYPE_NF3FIFO nfs_ftype = 7
+)
+
+// Backing billy.FS doesn't support creation of
+// char, block, socket, or fifo pipe nodes
+func onMknod(ctx context.Context, w *response, userHandle Handler) error {
+	w.errorFmt = wccDataErrorFormatter
+	obj := DirOpArg{}
+	err := xdr.Read(w.req.Body, &obj)
+	if err != nil {
+		return &NFSStatusError{NFSStatusInval, err}
+	}
+
+	ftype, err := xdr.ReadUint32(w.req.Body)
+	if err != nil {
+		return &NFSStatusError{NFSStatusInval, err}
+	}
+
+	// see if the filesystem supports mknod
+	fs, path, err := userHandle.FromHandle(obj.Handle)
+	if err != nil {
+		return &NFSStatusError{NFSStatusStale, err}
+	}
+
+	if !CapabilityCheck(fs, billy.WriteCapability) {
+		return &NFSStatusError{NFSStatusROFS, os.ErrPermission}
+	}
+
+	c := userHandle.Change(fs)
+	if c == nil {
+		return &NFSStatusError{NFSStatusAccess, os.ErrPermission}
+	}
+	cu, ok := c.(UnixChange)
+	if !ok {
+		return &NFSStatusError{NFSStatusAccess, os.ErrPermission}
+	}
+
+	if len(string(obj.Filename)) > PathNameMax {
+		return &NFSStatusError{NFSStatusNameTooLong, os.ErrInvalid}
+	}
+
+	newFilePath := fs.Join(append(path, string(obj.Filename))...)
+	if _, err := fs.Stat(ctx, newFilePath); err == nil {
+		return &NFSStatusError{NFSStatusExist, os.ErrExist}
+	}
+	parent, err := fs.Stat(ctx, fs.Join(path...))
+	if err != nil {
+		return &NFSStatusError{NFSStatusAccess, err}
+	} else if !parent.IsDir() {
+		return &NFSStatusError{NFSStatusNotDir, nil}
+	}
+	fp := userHandle.ToHandle(fs, append(path, string(obj.Filename)))
+
+	switch nfs_ftype(ftype) {
+	case FTYPE_NF3CHR:
+	case FTYPE_NF3BLK:
+		// read devicedata3 = {sattr3, specdata3}
+		attrs, err := ReadSetFileAttributes(w.req.Body)
+		if err != nil {
+			return &NFSStatusError{NFSStatusInval, err}
+		}
+		specData1, err := xdr.ReadUint32(w.req.Body)
+		if err != nil {
+			return &NFSStatusError{NFSStatusInval, err}
+		}
+		specData2, err := xdr.ReadUint32(w.req.Body)
+		if err != nil {
+			return &NFSStatusError{NFSStatusInval, err}
+		}
+
+		err = cu.Mknod(ctx, newFilePath, uint32(attrs.Mode(parent.Mode())), specData1, specData2)
+		if err != nil {
+			return &NFSStatusError{NFSStatusAccess, err}
+		}
+		if err = attrs.Apply(ctx, cu, fs, newFilePath); err != nil {
+			return &NFSStatusError{NFSStatusServerFault, err}
+		}
+
+	case FTYPE_NF3SOCK:
+		// read sattr3
+		attrs, err := ReadSetFileAttributes(w.req.Body)
+		if err != nil {
+			return &NFSStatusError{NFSStatusInval, err}
+		}
+		if err := cu.Socket(ctx, newFilePath); err != nil {
+			return &NFSStatusError{NFSStatusAccess, err}
+		}
+		if err = attrs.Apply(ctx, cu, fs, newFilePath); err != nil {
+			return &NFSStatusError{NFSStatusServerFault, err}
+		}
+
+	case FTYPE_NF3FIFO:
+		// read sattr3
+		attrs, err := ReadSetFileAttributes(w.req.Body)
+		if err != nil {
+			return &NFSStatusError{NFSStatusInval, err}
+		}
+		err = cu.Mkfifo(ctx, newFilePath, uint32(attrs.Mode(parent.Mode())))
+		if err != nil {
+			return &NFSStatusError{NFSStatusAccess, err}
+		}
+		if err = attrs.Apply(ctx, cu, fs, newFilePath); err != nil {
+			return &NFSStatusError{NFSStatusServerFault, err}
+		}
+
+	default:
+		return &NFSStatusError{NFSStatusBadType, os.ErrInvalid}
+		// end of input.
+	}
+
+	writer := bytes.NewBuffer([]byte{})
+	if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	// "handle follows"
+	if err := xdr.Write(writer, uint32(1)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	// fh3
+	if err := xdr.Write(writer, fp); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	// attr
+	if err := WritePostOpAttrs(writer, tryStat(ctx, fs, append(path, string(obj.Filename)))); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	// wcc
+	if err := WriteWcc(writer, nil, tryStat(ctx, fs, path)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	if err := w.Write(writer.Bytes()); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	return nil
+}
diff --git a/pkg/go-nfs/nfs_onpathconf.go b/pkg/go-nfs/nfs_onpathconf.go
new file mode 100644
index 0000000..1771b60
--- /dev/null
+++ b/pkg/go-nfs/nfs_onpathconf.go
@@ -0,0 +1,55 @@
+package nfs
+
+import (
+	"bytes"
+	"context"
+
+	"github.com/willscott/go-nfs-client/nfs/xdr"
+)
+
+// PathNameMax is the maximum length for a file name
+const PathNameMax = 255
+
+func onPathConf(ctx context.Context, w *response, userHandle Handler) error {
+	roothandle, err := xdr.ReadOpaque(w.req.Body)
+	if err != nil {
+		return &NFSStatusError{NFSStatusInval, err}
+	}
+	fs, path, err := userHandle.FromHandle(roothandle)
+	if err != nil {
+		return &NFSStatusError{NFSStatusStale, err}
+	}
+
+	writer := bytes.NewBuffer([]byte{})
+	if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if err := WritePostOpAttrs(writer, tryStat(ctx, fs, path)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	type PathConf struct {
+		LinkMax         uint32
+		NameMax         uint32
+		NoTrunc         uint32
+		ChownRestricted uint32
+		CaseInsensitive uint32
+		CasePreserving  uint32
+	}
+
+	defaults := PathConf{
+		LinkMax:         1,
+		NameMax:         PathNameMax,
+		NoTrunc:         1,
+		ChownRestricted: 0,
+		CaseInsensitive: 0,
+		CasePreserving:  1,
+	}
+	if err := xdr.Write(writer, defaults); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if err := w.Write(writer.Bytes()); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	return nil
+}
diff --git a/pkg/go-nfs/nfs_onread.go b/pkg/go-nfs/nfs_onread.go
new file mode 100644
index 0000000..8616acd
--- /dev/null
+++ b/pkg/go-nfs/nfs_onread.go
@@ -0,0 +1,97 @@
+package nfs
+
+import (
+	"bytes"
+	"context"
+	"errors"
+	"io"
+	"os"
+
+	"github.com/willscott/go-nfs-client/nfs/xdr"
+)
+
+type nfsReadArgs struct {
+	Handle []byte
+	Offset uint64
+	Count  uint32
+}
+
+type nfsReadResponse struct {
+	Count uint32
+	EOF   uint32
+	Data  []byte
+}
+
+// MaxRead is the advertised largest buffer the server is willing to read
+const MaxRead = 1 << 24
+
+// CheckRead is a size where - if a request to read is larger than this,
+// the server will stat the file to learn it's actual size before allocating
+// a buffer to read into.
+const CheckRead = 1 << 15
+
+func onRead(ctx context.Context, w *response, userHandle Handler) error {
+	w.errorFmt = opAttrErrorFormatter
+	var obj nfsReadArgs
+	err := xdr.Read(w.req.Body, &obj)
+	if err != nil {
+		return &NFSStatusError{NFSStatusInval, err}
+	}
+	fs, path, err := userHandle.FromHandle(obj.Handle)
+	if err != nil {
+		return &NFSStatusError{NFSStatusStale, err}
+	}
+
+	fh, err := fs.Open(ctx, fs.Join(path...))
+	if err != nil {
+		if os.IsNotExist(err) {
+			return &NFSStatusError{NFSStatusNoEnt, err}
+		}
+		if errors.Is(err, context.DeadlineExceeded) {
+			return &NFSStatusError{NFSStatusJukebox, err}
+		}
+		return &NFSStatusError{NFSStatusAccess, err}
+	}
+
+	resp := nfsReadResponse{}
+
+	if obj.Count > CheckRead {
+		info, err := fs.Stat(ctx, fs.Join(path...))
+		if err != nil {
+			return &NFSStatusError{NFSStatusAccess, err}
+		}
+		if info.Size()-int64(obj.Offset) < int64(obj.Count) {
+			obj.Count = uint32(uint64(info.Size()) - obj.Offset)
+		}
+	}
+	if obj.Count > MaxRead {
+		obj.Count = MaxRead
+	}
+	resp.Data = make([]byte, obj.Count)
+	// todo: multiple reads if size isn't full
+	cnt, err := fh.ReadAt(ctx, resp.Data, int64(obj.Offset))
+	if err != nil && !errors.Is(err, io.EOF) {
+		return &NFSStatusError{NFSStatusIO, err}
+	}
+	resp.Count = uint32(cnt)
+	resp.Data = resp.Data[:resp.Count]
+	if errors.Is(err, io.EOF) {
+		resp.EOF = 1
+	}
+
+	writer := bytes.NewBuffer([]byte{})
+	if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if err := WritePostOpAttrs(writer, tryStat(ctx, fs, path)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	if err := xdr.Write(writer, resp); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if err := w.Write(writer.Bytes()); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	return nil
+}
diff --git a/pkg/go-nfs/nfs_onreaddir.go b/pkg/go-nfs/nfs_onreaddir.go
new file mode 100644
index 0000000..2769d02
--- /dev/null
+++ b/pkg/go-nfs/nfs_onreaddir.go
@@ -0,0 +1,195 @@
+package nfs
+
+import (
+	"bytes"
+	"context"
+	"crypto/sha256"
+	"encoding/binary"
+	"errors"
+	"io"
+	"io/fs"
+	"os"
+	"path"
+	"sort"
+
+	"github.com/willscott/go-nfs-client/nfs/xdr"
+)
+
+type readDirArgs struct {
+	Handle      []byte
+	Cookie      uint64
+	CookieVerif uint64
+	Count       uint32
+}
+
+type readDirEntity struct {
+	FileID uint64
+	Name   []byte
+	Cookie uint64
+	Next   bool
+}
+
+func onReadDir(ctx context.Context, w *response, userHandle Handler) error {
+	w.errorFmt = opAttrErrorFormatter
+	obj := readDirArgs{}
+	err := xdr.Read(w.req.Body, &obj)
+	if err != nil {
+		return &NFSStatusError{NFSStatusInval, err}
+	}
+
+	if obj.Count < 1024 {
+		return &NFSStatusError{NFSStatusTooSmall, io.ErrShortBuffer}
+	}
+
+	fs, p, err := userHandle.FromHandle(obj.Handle)
+	if err != nil {
+		return &NFSStatusError{NFSStatusStale, err}
+	}
+
+	contents, verifier, err := getDirListingWithVerifier(ctx, userHandle, obj.Handle, obj.CookieVerif)
+	if err != nil {
+		return err
+	}
+	if obj.Cookie > 0 && obj.CookieVerif > 0 && verifier != obj.CookieVerif {
+		return &NFSStatusError{NFSStatusBadCookie, nil}
+	}
+
+	entities := make([]readDirEntity, 0)
+	maxBytes := uint32(100) // conservative overhead measure
+
+	started := obj.Cookie == 0
+	if started {
+		// add '.' and '..' to entities
+		dotdotFileID := uint64(0)
+		if len(p) > 0 {
+			dda := tryStat(ctx, fs, p[0:len(p)-1])
+			if dda != nil {
+				dotdotFileID = dda.Fileid
+			}
+		}
+		dotFileID := uint64(0)
+		da := tryStat(ctx, fs, p)
+		if da != nil {
+			dotFileID = da.Fileid
+		}
+		entities = append(entities,
+			readDirEntity{Name: []byte("."), Cookie: 0, Next: true, FileID: dotFileID},
+			readDirEntity{Name: []byte(".."), Cookie: 1, Next: true, FileID: dotdotFileID},
+		)
+	}
+
+	eof := true
+	maxEntities := userHandle.HandleLimit() / 2
+	for i, c := range contents {
+		// cookie equates to index within contents + 2 (for '.' and '..')
+		cookie := uint64(i + 2)
+		if started {
+			maxBytes += 512 // TODO: better estimation.
+			if maxBytes > obj.Count || len(entities) > maxEntities {
+				eof = false
+				break
+			}
+
+			attrs := ToFileAttribute(c, path.Join(append(p, c.Name())...))
+			entities = append(entities, readDirEntity{
+				FileID: attrs.Fileid,
+				Name:   []byte(c.Name()),
+				Cookie: cookie,
+				Next:   true,
+			})
+		} else if cookie == obj.Cookie {
+			started = true
+		}
+	}
+
+	writer := bytes.NewBuffer([]byte{})
+	if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if err := WritePostOpAttrs(writer, tryStat(ctx, fs, p)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	if err := xdr.Write(writer, verifier); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	if err := xdr.Write(writer, len(entities) > 0); err != nil { // next
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if len(entities) > 0 {
+		entities[len(entities)-1].Next = false
+		// no next for last entity
+
+		for _, e := range entities {
+			if err := xdr.Write(writer, e); err != nil {
+				return &NFSStatusError{NFSStatusServerFault, err}
+			}
+		}
+	}
+	if err := xdr.Write(writer, eof); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	// TODO: track writer size at this point to validate maxcount estimation and stop early if needed.
+
+	if err := w.Write(writer.Bytes()); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	return nil
+}
+
+func getDirListingWithVerifier(ctx context.Context, userHandle Handler, fsHandle []byte, verifier uint64) ([]fs.FileInfo, uint64, error) {
+	// figure out what directory it is.
+	fs, p, err := userHandle.FromHandle(fsHandle)
+	if err != nil {
+		return nil, 0, &NFSStatusError{NFSStatusStale, err}
+	}
+
+	path := fs.Join(p...)
+	// see if the verifier has this dir cached:
+	if vh, ok := userHandle.(CachingHandler); verifier != 0 && ok {
+		entries := vh.DataForVerifier(path, verifier)
+		if entries != nil {
+			return entries, verifier, nil
+		}
+	}
+	// load the entries.
+	contents, err := fs.ReadDir(ctx, path)
+	if err != nil {
+		if os.IsPermission(err) {
+			return nil, 0, &NFSStatusError{NFSStatusAccess, err}
+		}
+		if errors.Is(err, context.DeadlineExceeded) {
+			return nil, 0, &NFSStatusError{NFSStatusJukebox, err}
+		}
+		return nil, 0, &NFSStatusError{NFSStatusIO, err}
+	}
+
+	sort.Slice(contents, func(i, j int) bool {
+		return contents[i].Name() < contents[j].Name()
+	})
+
+	if vh, ok := userHandle.(CachingHandler); ok {
+		// let the user handler make a verifier if it can.
+		v := vh.VerifierFor(path, contents)
+		return contents, v, nil
+	}
+
+	id := hashPathAndContents(path, contents)
+	return contents, id, nil
+}
+
+func hashPathAndContents(path string, contents []fs.FileInfo) uint64 {
+	//calculate a cookie-verifier.
+	vHash := sha256.New()
+
+	// Add the path to avoid collisions of directories with the same content
+	vHash.Write([]byte(path))
+
+	for _, c := range contents {
+		vHash.Write([]byte(c.Name())) // Never fails according to the docs
+	}
+
+	verify := vHash.Sum(nil)[0:8]
+	return binary.BigEndian.Uint64(verify)
+}
diff --git a/pkg/go-nfs/nfs_onreaddirplus.go b/pkg/go-nfs/nfs_onreaddirplus.go
new file mode 100644
index 0000000..9fa49cb
--- /dev/null
+++ b/pkg/go-nfs/nfs_onreaddirplus.go
@@ -0,0 +1,153 @@
+package nfs
+
+import (
+	"bytes"
+	"context"
+	"path"
+
+	"github.com/willscott/go-nfs-client/nfs/xdr"
+)
+
+type readDirPlusArgs struct {
+	Handle      []byte
+	Cookie      uint64
+	CookieVerif uint64
+	DirCount    uint32
+	MaxCount    uint32
+}
+
+type readDirPlusEntity struct {
+	FileID     uint64
+	Name       []byte
+	Cookie     uint64
+	Attributes *FileAttribute `xdr:"optional"`
+	Handle     *[]byte        `xdr:"optional"`
+	Next       bool
+}
+
+func joinPath(parent []string, elements ...string) []string {
+	joinedPath := make([]string, 0, len(parent)+len(elements))
+	joinedPath = append(joinedPath, parent...)
+	joinedPath = append(joinedPath, elements...)
+	return joinedPath
+}
+
+func onReadDirPlus(ctx context.Context, w *response, userHandle Handler) error {
+	w.errorFmt = opAttrErrorFormatter
+	obj := readDirPlusArgs{}
+	if err := xdr.Read(w.req.Body, &obj); err != nil {
+		return &NFSStatusError{NFSStatusInval, err}
+	}
+
+	// in case of test, nfs-client send:
+	// DirCount = 512
+	// MaxCount = 4096
+	if obj.DirCount < 512 || obj.MaxCount < 4096 {
+		return &NFSStatusError{NFSStatusTooSmall, nil}
+	}
+
+	fs, p, err := userHandle.FromHandle(obj.Handle)
+	if err != nil {
+		return &NFSStatusError{NFSStatusStale, err}
+	}
+
+	contents, verifier, err := getDirListingWithVerifier(ctx, userHandle, obj.Handle, obj.CookieVerif)
+	if err != nil {
+		return err
+	}
+	if obj.Cookie > 0 && obj.CookieVerif > 0 && verifier != obj.CookieVerif {
+		return &NFSStatusError{NFSStatusBadCookie, nil}
+	}
+
+	entities := make([]readDirPlusEntity, 0)
+	dirBytes := uint32(0)
+	maxBytes := uint32(100) // conservative overhead measure
+
+	started := obj.Cookie == 0
+	if started {
+		// add '.' and '..' to entities
+		dotdotFileID := uint64(0)
+		if len(p) > 0 {
+			dda := tryStat(ctx, fs, p[0:len(p)-1])
+			if dda != nil {
+				dotdotFileID = dda.Fileid
+			}
+		}
+		dotFileID := uint64(0)
+		da := tryStat(ctx, fs, p)
+		if da != nil {
+			dotFileID = da.Fileid
+		}
+		entities = append(entities,
+			readDirPlusEntity{Name: []byte("."), Cookie: 0, Next: true, FileID: dotFileID, Attributes: da},
+			readDirPlusEntity{Name: []byte(".."), Cookie: 1, Next: true, FileID: dotdotFileID},
+		)
+	}
+
+	eof := true
+	maxEntities := userHandle.HandleLimit() / 2
+	fb := 0
+	fss := 0
+	for i, c := range contents {
+		// cookie equates to index within contents + 2 (for '.' and '..')
+		cookie := uint64(i + 2)
+		fb++
+		if started {
+			fss++
+			dirBytes += uint32(len(c.Name()) + 20)
+			maxBytes += 512 // TODO: better estimation.
+			if dirBytes > obj.DirCount || maxBytes > obj.MaxCount || len(entities) > maxEntities {
+				eof = false
+				break
+			}
+
+			filePath := joinPath(p, c.Name())
+			handle := userHandle.ToHandle(fs, filePath)
+			attrs := ToFileAttribute(c, path.Join(filePath...))
+			entities = append(entities, readDirPlusEntity{
+				FileID:     attrs.Fileid,
+				Name:       []byte(c.Name()),
+				Cookie:     cookie,
+				Attributes: attrs,
+				Handle:     &handle,
+				Next:       true,
+			})
+		} else if cookie == obj.Cookie {
+			started = true
+		}
+	}
+
+	writer := bytes.NewBuffer([]byte{})
+	if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if err := WritePostOpAttrs(writer, tryStat(ctx, fs, p)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if err := xdr.Write(writer, verifier); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	if err := xdr.Write(writer, len(entities) > 0); err != nil { // next
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if len(entities) > 0 {
+		entities[len(entities)-1].Next = false
+		// no next for last entity
+
+		for _, e := range entities {
+			if err := xdr.Write(writer, e); err != nil {
+				return &NFSStatusError{NFSStatusServerFault, err}
+			}
+		}
+	}
+	if err := xdr.Write(writer, eof); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	// TODO: track writer size at this point to validate maxcount estimation and stop early if needed.
+
+	if err := w.Write(writer.Bytes()); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	return nil
+}
diff --git a/pkg/go-nfs/nfs_onreadlink.go b/pkg/go-nfs/nfs_onreadlink.go
new file mode 100644
index 0000000..887047a
--- /dev/null
+++ b/pkg/go-nfs/nfs_onreadlink.go
@@ -0,0 +1,55 @@
+package nfs
+
+import (
+	"bytes"
+	"context"
+	"errors"
+	"os"
+
+	"github.com/willscott/go-nfs-client/nfs/xdr"
+)
+
+func onReadLink(ctx context.Context, w *response, userHandle Handler) error {
+	w.errorFmt = opAttrErrorFormatter
+	handle, err := xdr.ReadOpaque(w.req.Body)
+	if err != nil {
+		return &NFSStatusError{NFSStatusInval, err}
+	}
+	fs, path, err := userHandle.FromHandle(handle)
+	if err != nil {
+		return &NFSStatusError{NFSStatusStale, err}
+	}
+
+	out, err := fs.Readlink(ctx, fs.Join(path...))
+	if err != nil {
+		if info, err := fs.Stat(ctx, fs.Join(path...)); err == nil {
+			if info.Mode()&os.ModeSymlink == 0 {
+				return &NFSStatusError{NFSStatusInval, err}
+			}
+		}
+		if os.IsNotExist(err) {
+			return &NFSStatusError{NFSStatusNoEnt, err}
+		}
+		if errors.Is(err, context.DeadlineExceeded) {
+			return &NFSStatusError{NFSStatusJukebox, err}
+		}
+
+		return &NFSStatusError{NFSStatusAccess, err}
+	}
+
+	writer := bytes.NewBuffer([]byte{})
+	if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if err := WritePostOpAttrs(writer, tryStat(ctx, fs, path)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	if err := xdr.Write(writer, out); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if err := w.Write(writer.Bytes()); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	return nil
+}
diff --git a/pkg/go-nfs/nfs_onremove.go b/pkg/go-nfs/nfs_onremove.go
new file mode 100644
index 0000000..bffb446
--- /dev/null
+++ b/pkg/go-nfs/nfs_onremove.go
@@ -0,0 +1,85 @@
+package nfs
+
+import (
+	"bytes"
+	"context"
+	"errors"
+	"os"
+
+	"github.com/willscott/go-nfs-client/nfs/xdr"
+)
+
+func onRemove(ctx context.Context, w *response, userHandle Handler) error {
+	w.errorFmt = wccDataErrorFormatter
+	obj := DirOpArg{}
+	if err := xdr.Read(w.req.Body, &obj); err != nil {
+		return &NFSStatusError{NFSStatusInval, err}
+	}
+	fs, path, err := userHandle.FromHandle(obj.Handle)
+	if err != nil {
+		return &NFSStatusError{NFSStatusStale, err}
+	}
+
+	// TODO
+	// if !CapabilityCheck(fs, billy.WriteCapability) {
+	// 	return &NFSStatusError{NFSStatusROFS, os.ErrPermission}
+	// }
+
+	if len(string(obj.Filename)) > PathNameMax {
+		return &NFSStatusError{NFSStatusNameTooLong, nil}
+	}
+
+	fullPath := fs.Join(path...)
+	dirInfo, err := fs.Stat(ctx, fullPath)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return &NFSStatusError{NFSStatusNoEnt, err}
+		}
+		if os.IsPermission(err) {
+			return &NFSStatusError{NFSStatusAccess, err}
+		}
+		if errors.Is(err, context.DeadlineExceeded) {
+			return &NFSStatusError{NFSStatusJukebox, err}
+		}
+		return &NFSStatusError{NFSStatusIO, err}
+	}
+	if !dirInfo.IsDir() {
+		return &NFSStatusError{NFSStatusNotDir, nil}
+	}
+	preCacheData := ToFileAttribute(dirInfo, fullPath).AsCache()
+
+	toDelete := fs.Join(append(path, string(obj.Filename))...)
+	toDeleteHandle := userHandle.ToHandle(fs, append(path, string(obj.Filename)))
+
+	err = fs.Remove(ctx, toDelete)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return &NFSStatusError{NFSStatusNoEnt, err}
+		}
+		if os.IsPermission(err) {
+			return &NFSStatusError{NFSStatusAccess, err}
+		}
+		if errors.Is(err, context.DeadlineExceeded) {
+			return &NFSStatusError{NFSStatusJukebox, err}
+		}
+		return &NFSStatusError{NFSStatusIO, err}
+	}
+
+	if err := userHandle.InvalidateHandle(fs, toDeleteHandle); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	writer := bytes.NewBuffer([]byte{})
+	if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	if err := WriteWcc(writer, preCacheData, tryStat(ctx, fs, path)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	if err := w.Write(writer.Bytes()); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	return nil
+}
diff --git a/pkg/go-nfs/nfs_onrename.go b/pkg/go-nfs/nfs_onrename.go
new file mode 100644
index 0000000..dc0d463
--- /dev/null
+++ b/pkg/go-nfs/nfs_onrename.go
@@ -0,0 +1,120 @@
+package nfs
+
+import (
+	"bytes"
+	"context"
+	"errors"
+	"os"
+	"reflect"
+
+	"github.com/go-git/go-billy/v5"
+	"github.com/willscott/go-nfs-client/nfs/xdr"
+)
+
+var doubleWccErrorBody = [16]byte{}
+
+func onRename(ctx context.Context, w *response, userHandle Handler) error {
+	w.errorFmt = errFormatterWithBody(doubleWccErrorBody[:])
+	from := DirOpArg{}
+	err := xdr.Read(w.req.Body, &from)
+	if err != nil {
+		return &NFSStatusError{NFSStatusInval, err}
+	}
+	fs, fromPath, err := userHandle.FromHandle(from.Handle)
+	if err != nil {
+		return &NFSStatusError{NFSStatusStale, err}
+	}
+
+	to := DirOpArg{}
+	if err = xdr.Read(w.req.Body, &to); err != nil {
+		return &NFSStatusError{NFSStatusInval, err}
+	}
+	fs2, toPath, err := userHandle.FromHandle(to.Handle)
+	if err != nil {
+		return &NFSStatusError{NFSStatusStale, err}
+	}
+	// check the two fs are the same
+	if !reflect.DeepEqual(fs, fs2) {
+		return &NFSStatusError{NFSStatusNotSupp, os.ErrPermission}
+	}
+
+	if !CapabilityCheck(fs, billy.WriteCapability) {
+		return &NFSStatusError{NFSStatusROFS, os.ErrPermission}
+	}
+
+	if len(string(from.Filename)) > PathNameMax || len(string(to.Filename)) > PathNameMax {
+		return &NFSStatusError{NFSStatusNameTooLong, os.ErrInvalid}
+	}
+
+	fromDirPath := fs.Join(fromPath...)
+	fromDirInfo, err := fs.Stat(ctx, fromDirPath)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return &NFSStatusError{NFSStatusNoEnt, err}
+		}
+		if errors.Is(err, context.DeadlineExceeded) {
+			return &NFSStatusError{NFSStatusJukebox, err}
+		}
+		return &NFSStatusError{NFSStatusIO, err}
+	}
+	if !fromDirInfo.IsDir() {
+		return &NFSStatusError{NFSStatusNotDir, nil}
+	}
+	preCacheData := ToFileAttribute(fromDirInfo, fromDirPath).AsCache()
+
+	toDirPath := fs.Join(toPath...)
+	toDirInfo, err := fs.Stat(ctx, toDirPath)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return &NFSStatusError{NFSStatusNoEnt, err}
+		}
+		if errors.Is(err, context.DeadlineExceeded) {
+			return &NFSStatusError{NFSStatusJukebox, err}
+		}
+		return &NFSStatusError{NFSStatusIO, err}
+	}
+	if !toDirInfo.IsDir() {
+		return &NFSStatusError{NFSStatusNotDir, nil}
+	}
+	preDestData := ToFileAttribute(toDirInfo, toDirPath).AsCache()
+
+	oldHandle := userHandle.ToHandle(fs, append(fromPath, string(from.Filename)))
+
+	fromLoc := fs.Join(append(fromPath, string(from.Filename))...)
+	toLoc := fs.Join(append(toPath, string(to.Filename))...)
+
+	err = fs.Rename(ctx, fromLoc, toLoc)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return &NFSStatusError{NFSStatusNoEnt, err}
+		}
+		if os.IsPermission(err) {
+			return &NFSStatusError{NFSStatusAccess, err}
+		}
+		if errors.Is(err, context.DeadlineExceeded) {
+			return &NFSStatusError{NFSStatusJukebox, err}
+		}
+		return &NFSStatusError{NFSStatusIO, err}
+	}
+
+	if err := userHandle.InvalidateHandle(fs, oldHandle); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	writer := bytes.NewBuffer([]byte{})
+	if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	if err := WriteWcc(writer, preCacheData, tryStat(ctx, fs, fromPath)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if err := WriteWcc(writer, preDestData, tryStat(ctx, fs, toPath)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	if err := w.Write(writer.Bytes()); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	return nil
+}
diff --git a/pkg/go-nfs/nfs_onrmdir.go b/pkg/go-nfs/nfs_onrmdir.go
new file mode 100644
index 0000000..12e3571
--- /dev/null
+++ b/pkg/go-nfs/nfs_onrmdir.go
@@ -0,0 +1,9 @@
+package nfs
+
+import (
+	"context"
+)
+
+func onRmDir(ctx context.Context, w *response, userHandle Handler) error {
+	return onRemove(ctx, w, userHandle)
+}
diff --git a/pkg/go-nfs/nfs_onsetattr.go b/pkg/go-nfs/nfs_onsetattr.go
new file mode 100644
index 0000000..cc2c202
--- /dev/null
+++ b/pkg/go-nfs/nfs_onsetattr.go
@@ -0,0 +1,80 @@
+package nfs
+
+import (
+	"bytes"
+	"context"
+	"errors"
+	"os"
+
+	"github.com/go-git/go-billy/v5"
+	"github.com/willscott/go-nfs-client/nfs/xdr"
+)
+
+func onSetAttr(ctx context.Context, w *response, userHandle Handler) error {
+	w.errorFmt = wccDataErrorFormatter
+	handle, err := xdr.ReadOpaque(w.req.Body)
+	if err != nil {
+		return &NFSStatusError{NFSStatusInval, err}
+	}
+
+	fs, path, err := userHandle.FromHandle(handle)
+	if err != nil {
+		return &NFSStatusError{NFSStatusStale, err}
+	}
+	attrs, err := ReadSetFileAttributes(w.req.Body)
+	if err != nil {
+		return &NFSStatusError{NFSStatusInval, err}
+	}
+
+	fullPath := fs.Join(path...)
+	info, err := fs.Lstat(ctx, fullPath)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return &NFSStatusError{NFSStatusNoEnt, err}
+		}
+		if errors.Is(err, context.DeadlineExceeded) {
+			return &NFSStatusError{NFSStatusJukebox, err}
+		}
+		return &NFSStatusError{NFSStatusAccess, err}
+	}
+
+	// see if there's a "guard"
+	if guard, err := xdr.ReadUint32(w.req.Body); err != nil {
+		return &NFSStatusError{NFSStatusInval, err}
+	} else if guard != 0 {
+		// read the ctime.
+		t := FileTime{}
+		if err := xdr.Read(w.req.Body, &t); err != nil {
+			return &NFSStatusError{NFSStatusInval, err}
+		}
+		attr := ToFileAttribute(info, fullPath)
+		if t != attr.Ctime {
+			return &NFSStatusError{NFSStatusNotSync, nil}
+		}
+	}
+
+	if !CapabilityCheck(fs, billy.WriteCapability) {
+		return &NFSStatusError{NFSStatusROFS, os.ErrPermission}
+	}
+
+	changer := userHandle.Change(fs)
+	if err := attrs.Apply(ctx, changer, fs, fs.Join(path...)); err != nil {
+		// Already an nfsstatuserror
+		return err
+	}
+
+	preAttr := ToFileAttribute(info, fullPath).AsCache()
+
+	writer := bytes.NewBuffer([]byte{})
+	if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if err := WriteWcc(writer, preAttr, tryStat(ctx, fs, path)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	if err := w.Write(writer.Bytes()); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	return nil
+}
diff --git a/pkg/go-nfs/nfs_onsymlink.go b/pkg/go-nfs/nfs_onsymlink.go
new file mode 100644
index 0000000..4b728c6
--- /dev/null
+++ b/pkg/go-nfs/nfs_onsymlink.go
@@ -0,0 +1,88 @@
+package nfs
+
+import (
+	"bytes"
+	"context"
+	"os"
+
+	"github.com/go-git/go-billy/v5"
+	"github.com/willscott/go-nfs-client/nfs/xdr"
+)
+
+func onSymlink(ctx context.Context, w *response, userHandle Handler) error {
+	w.errorFmt = wccDataErrorFormatter
+	obj := DirOpArg{}
+	err := xdr.Read(w.req.Body, &obj)
+	if err != nil {
+		return &NFSStatusError{NFSStatusInval, err}
+	}
+	attrs, err := ReadSetFileAttributes(w.req.Body)
+	if err != nil {
+		return &NFSStatusError{NFSStatusInval, err}
+	}
+
+	target, err := xdr.ReadOpaque(w.req.Body)
+	if err != nil {
+		return &NFSStatusError{NFSStatusInval, err}
+	}
+
+	fs, path, err := userHandle.FromHandle(obj.Handle)
+	if err != nil {
+		return &NFSStatusError{NFSStatusStale, err}
+	}
+	if !CapabilityCheck(fs, billy.WriteCapability) {
+		return &NFSStatusError{NFSStatusROFS, os.ErrPermission}
+	}
+
+	if len(string(obj.Filename)) > PathNameMax {
+		return &NFSStatusError{NFSStatusNameTooLong, os.ErrInvalid}
+	}
+
+	newFilePath := fs.Join(append(path, string(obj.Filename))...)
+	if _, err := fs.Stat(ctx, newFilePath); err == nil {
+		return &NFSStatusError{NFSStatusExist, os.ErrExist}
+	}
+	if s, err := fs.Stat(ctx, fs.Join(path...)); err != nil {
+		return &NFSStatusError{NFSStatusAccess, err}
+	} else if !s.IsDir() {
+		return &NFSStatusError{NFSStatusNotDir, nil}
+	}
+
+	err = fs.Symlink(ctx, string(target), newFilePath)
+	if err != nil {
+		return &NFSStatusError{NFSStatusAccess, err}
+	}
+
+	fp := userHandle.ToHandle(fs, append(path, string(obj.Filename)))
+	changer := userHandle.Change(fs)
+	if changer != nil {
+		if err := attrs.Apply(ctx, changer, fs, newFilePath); err != nil {
+			return &NFSStatusError{NFSStatusIO, err}
+		}
+	}
+
+	writer := bytes.NewBuffer([]byte{})
+	if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	// "handle follows"
+	if err := xdr.Write(writer, uint32(1)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if err := xdr.Write(writer, fp); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if err := WritePostOpAttrs(writer, tryStat(ctx, fs, append(path, string(obj.Filename)))); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	if err := WriteWcc(writer, nil, tryStat(ctx, fs, path)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	if err := w.Write(writer.Bytes()); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	return nil
+}
diff --git a/pkg/go-nfs/nfs_onwrite.go b/pkg/go-nfs/nfs_onwrite.go
new file mode 100644
index 0000000..cfe2eb4
--- /dev/null
+++ b/pkg/go-nfs/nfs_onwrite.go
@@ -0,0 +1,116 @@
+package nfs
+
+import (
+	"bytes"
+	"context"
+	"errors"
+	"io"
+	"math"
+	"os"
+
+	"github.com/willscott/go-nfs-client/nfs/xdr"
+)
+
+// writeStability is the level of durability requested with the write
+type writeStability uint32
+
+const (
+	unstable writeStability = 0
+	dataSync writeStability = 1
+	fileSync writeStability = 2
+)
+
+type writeArgs struct {
+	Handle []byte
+	Offset uint64
+	Count  uint32
+	How    uint32
+	Data   []byte
+}
+
+func onWrite(ctx context.Context, w *response, userHandle Handler) error {
+	w.errorFmt = wccDataErrorFormatter
+	var req writeArgs
+	if err := xdr.Read(w.req.Body, &req); err != nil {
+		return &NFSStatusError{NFSStatusInval, err}
+	}
+
+	fs, path, err := userHandle.FromHandle(req.Handle)
+	if err != nil {
+		return &NFSStatusError{NFSStatusStale, err}
+	}
+	// TODO
+	// if !CapabilityCheck(fs, billy.WriteCapability) {
+	// 	return &NFSStatusError{NFSStatusROFS, os.ErrPermission}
+	// }
+	if len(req.Data) > math.MaxInt32 || req.Count > math.MaxInt32 {
+		return &NFSStatusError{NFSStatusFBig, os.ErrInvalid}
+	}
+	if req.How != uint32(unstable) && req.How != uint32(dataSync) && req.How != uint32(fileSync) {
+		return &NFSStatusError{NFSStatusInval, os.ErrInvalid}
+	}
+
+	// stat first for pre-op wcc.
+	fullPath := fs.Join(path...)
+	info, err := fs.Stat(ctx, fullPath)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return &NFSStatusError{NFSStatusNoEnt, err}
+		}
+		if errors.Is(err, context.DeadlineExceeded) {
+			return &NFSStatusError{NFSStatusJukebox, err}
+		}
+		return &NFSStatusError{NFSStatusAccess, err}
+	}
+	if !info.Mode().IsRegular() {
+		return &NFSStatusError{NFSStatusInval, os.ErrInvalid}
+	}
+	preOpCache := ToFileAttribute(info, fullPath).AsCache()
+
+	// now the actual op.
+	file, err := fs.OpenFile(ctx, fs.Join(path...), os.O_RDWR, info.Mode().Perm())
+	if err != nil {
+		return &NFSStatusError{NFSStatusAccess, err}
+	}
+	if req.Offset > 0 {
+		if _, err := file.Seek(int64(req.Offset), io.SeekStart); err != nil {
+			return &NFSStatusError{NFSStatusIO, err}
+		}
+	}
+	end := req.Count
+	if len(req.Data) < int(end) {
+		end = uint32(len(req.Data))
+	}
+	writtenCount, err := file.Write(ctx, req.Data[:end])
+	if err != nil {
+		Log.Errorf("Error writing: %v", err)
+		return &NFSStatusError{NFSStatusIO, err}
+	}
+	if err := file.Close(ctx); err != nil {
+		Log.Errorf("error closing: %v", err)
+		return &NFSStatusError{NFSStatusIO, err}
+	}
+
+	writer := bytes.NewBuffer([]byte{})
+	if err := xdr.Write(writer, uint32(NFSStatusOk)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	if err := WriteWcc(writer, preOpCache, tryStat(ctx, fs, path)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if err := xdr.Write(writer, uint32(writtenCount)); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if err := xdr.Write(writer, fileSync); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	if err := xdr.Write(writer, w.Server.ID); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+
+	if err := w.Write(writer.Bytes()); err != nil {
+		return &NFSStatusError{NFSStatusServerFault, err}
+	}
+	return nil
+}
diff --git a/pkg/go-nfs/nfs_test.go b/pkg/go-nfs/nfs_test.go
new file mode 100644
index 0000000..8a79752
--- /dev/null
+++ b/pkg/go-nfs/nfs_test.go
@@ -0,0 +1,293 @@
+package nfs_test
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"net"
+	"reflect"
+	"sort"
+	"testing"
+
+	nfs "git.kmsign.ru/royalcat/tstor/pkg/go-nfs"
+	"git.kmsign.ru/royalcat/tstor/pkg/go-nfs/helpers"
+	"git.kmsign.ru/royalcat/tstor/pkg/go-nfs/helpers/memfs"
+
+	nfsc "github.com/willscott/go-nfs-client/nfs"
+	rpc "github.com/willscott/go-nfs-client/nfs/rpc"
+	"github.com/willscott/go-nfs-client/nfs/util"
+	"github.com/willscott/go-nfs-client/nfs/xdr"
+)
+
+func TestNFS(t *testing.T) {
+	ctx := context.Background()
+
+	if testing.Verbose() {
+		util.DefaultLogger.SetDebug(true)
+	}
+
+	// make an empty in-memory server.
+	listener, err := net.Listen("tcp", "localhost:0")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	mem := helpers.WrapBillyFS(memfs.New())
+	// File needs to exist in the root for memfs to acknowledge the root exists.
+	_, _ = mem.Create(ctx, "/test")
+
+	handler := helpers.NewNullAuthHandler(mem)
+	cacheHelper := helpers.NewCachingHandler(handler, 1024)
+	go func() {
+		_ = nfs.Serve(listener, cacheHelper)
+	}()
+
+	c, err := rpc.DialTCP(listener.Addr().Network(), listener.Addr().(*net.TCPAddr).String(), false)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer c.Close()
+
+	var mounter nfsc.Mount
+	mounter.Client = c
+	target, err := mounter.Mount("/", rpc.AuthNull)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer func() {
+		_ = mounter.Unmount()
+	}()
+
+	_, err = target.FSInfo()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Validate sample file creation
+	_, err = target.Create("/helloworld.txt", 0666)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if info, err := mem.Stat(ctx, "/helloworld.txt"); err != nil {
+		t.Fatal(err)
+	} else {
+		if info.Size() != 0 || info.Mode().Perm() != 0666 {
+			t.Fatal("incorrect creation.")
+		}
+	}
+
+	// Validate writing to a file.
+	f, err := target.OpenFile("/helloworld.txt", 0666)
+	if err != nil {
+		t.Fatal(err)
+	}
+	b := []byte("hello world")
+	_, err = f.Write(b)
+	if err != nil {
+		t.Fatal(err)
+	}
+	mf, _ := mem.Open(ctx, "/helloworld.txt")
+	buf := make([]byte, len(b))
+	if _, err = mf.Read(ctx, buf[:]); err != nil {
+		t.Fatal(err)
+	}
+	if !bytes.Equal(buf, b) {
+		t.Fatal("written does not match expected")
+	}
+
+	// for test nfs.ReadDirPlus in case of many files
+	dirF1, err := mem.ReadDir(ctx, "/")
+	if err != nil {
+		t.Fatal(err)
+	}
+	shouldBeNames := []string{}
+	for _, f := range dirF1 {
+		shouldBeNames = append(shouldBeNames, f.Name())
+	}
+	for i := 0; i < 2000; i++ {
+		fName := fmt.Sprintf("f-%04d.txt", i)
+		shouldBeNames = append(shouldBeNames, fName)
+		f, err := mem.Create(ctx, fName)
+		if err != nil {
+			t.Fatal(err)
+		}
+		f.Close(ctx)
+	}
+
+	manyEntitiesPlus, err := target.ReadDirPlus("/")
+	if err != nil {
+		t.Fatal(err)
+	}
+	actualBeNamesPlus := []string{}
+	for _, e := range manyEntitiesPlus {
+		actualBeNamesPlus = append(actualBeNamesPlus, e.Name())
+	}
+
+	as := sort.StringSlice(shouldBeNames)
+	bs := sort.StringSlice(actualBeNamesPlus)
+	as.Sort()
+	bs.Sort()
+	if !reflect.DeepEqual(as, bs) {
+		t.Fatal("nfs.ReadDirPlus error")
+	}
+
+	// for test nfs.ReadDir in case of many files
+	manyEntities, err := readDir(target, "/")
+	if err != nil {
+		t.Fatal(err)
+	}
+	actualBeNames := []string{}
+	for _, e := range manyEntities {
+		actualBeNames = append(actualBeNames, e.FileName)
+	}
+
+	as2 := sort.StringSlice(shouldBeNames)
+	bs2 := sort.StringSlice(actualBeNames)
+	as2.Sort()
+	bs2.Sort()
+	if !reflect.DeepEqual(as2, bs2) {
+		fmt.Printf("should be %v\n", as2)
+		fmt.Printf("actual be %v\n", bs2)
+		t.Fatal("nfs.ReadDir error")
+	}
+
+	// confirm rename works as expected
+	oldFA, _, err := target.Lookup("/f-0010.txt", false)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if err := target.Rename("/f-0010.txt", "/g-0010.txt"); err != nil {
+		t.Fatal(err)
+	}
+	new, _, err := target.Lookup("/g-0010.txt", false)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if new.Sys() != oldFA.Sys() {
+		t.Fatal("rename failed to update")
+	}
+	_, _, err = target.Lookup("/f-0010.txt", false)
+	if err == nil {
+		t.Fatal("old handle should be invalid")
+	}
+
+	// for test nfs.ReadDirPlus in case of empty directory
+	_, err = target.Mkdir("/empty", 0755)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	emptyEntitiesPlus, err := target.ReadDirPlus("/empty")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(emptyEntitiesPlus) != 0 {
+		t.Fatal("nfs.ReadDirPlus error reading empty dir")
+	}
+
+	// for test nfs.ReadDir in case of empty directory
+	emptyEntities, err := readDir(target, "/empty")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(emptyEntities) != 0 {
+		t.Fatal("nfs.ReadDir error reading empty dir")
+	}
+}
+
+type readDirEntry struct {
+	FileId   uint64
+	FileName string
+	Cookie   uint64
+}
+
+// readDir implementation "appropriated" from go-nfs-client implementation of READDIRPLUS
+func readDir(target *nfsc.Target, dir string) ([]*readDirEntry, error) {
+	_, fh, err := target.Lookup(dir)
+	if err != nil {
+		return nil, err
+	}
+
+	type readDirArgs struct {
+		rpc.Header
+		Handle      []byte
+		Cookie      uint64
+		CookieVerif uint64
+		Count       uint32
+	}
+
+	type readDirList struct {
+		IsSet bool         `xdr:"union"`
+		Entry readDirEntry `xdr:"unioncase=1"`
+	}
+
+	type readDirListOK struct {
+		DirAttrs   nfsc.PostOpAttr
+		CookieVerf uint64
+	}
+
+	cookie := uint64(0)
+	cookieVerf := uint64(0)
+	eof := false
+
+	var entries []*readDirEntry
+	for !eof {
+		res, err := target.Call(&readDirArgs{
+			Header: rpc.Header{
+				Rpcvers: 2,
+				Vers:    nfsc.Nfs3Vers,
+				Prog:    nfsc.Nfs3Prog,
+				Proc:    uint32(nfs.NFSProcedureReadDir),
+				Cred:    rpc.AuthNull,
+				Verf:    rpc.AuthNull,
+			},
+			Handle:      fh,
+			Cookie:      cookie,
+			CookieVerif: cookieVerf,
+			Count:       4096,
+		})
+		if err != nil {
+			return nil, err
+		}
+
+		status, err := xdr.ReadUint32(res)
+		if err != nil {
+			return nil, err
+		}
+
+		if err = nfsc.NFS3Error(status); err != nil {
+			return nil, err
+		}
+
+		dirListOK := new(readDirListOK)
+		if err = xdr.Read(res, dirListOK); err != nil {
+			return nil, err
+		}
+
+		for {
+			var item readDirList
+			if err = xdr.Read(res, &item); err != nil {
+				return nil, err
+			}
+
+			if !item.IsSet {
+				break
+			}
+
+			cookie = item.Entry.Cookie
+			if item.Entry.FileName == "." || item.Entry.FileName == ".." {
+				continue
+			}
+			entries = append(entries, &item.Entry)
+		}
+
+		if err = xdr.Read(res, &eof); err != nil {
+			return nil, err
+		}
+
+		cookieVerf = dirListOK.CookieVerf
+	}
+
+	return entries, nil
+}
diff --git a/pkg/go-nfs/nfsinterface.go b/pkg/go-nfs/nfsinterface.go
new file mode 100644
index 0000000..cf439af
--- /dev/null
+++ b/pkg/go-nfs/nfsinterface.go
@@ -0,0 +1,188 @@
+package nfs
+
+// NFSProcedure is the valid RPC calls for the nfs service.
+type NFSProcedure uint32
+
+// NfsProcedure Codes
+const (
+	NFSProcedureNull NFSProcedure = iota
+	NFSProcedureGetAttr
+	NFSProcedureSetAttr
+	NFSProcedureLookup
+	NFSProcedureAccess
+	NFSProcedureReadlink
+	NFSProcedureRead
+	NFSProcedureWrite
+	NFSProcedureCreate
+	NFSProcedureMkDir
+	NFSProcedureSymlink
+	NFSProcedureMkNod
+	NFSProcedureRemove
+	NFSProcedureRmDir
+	NFSProcedureRename
+	NFSProcedureLink
+	NFSProcedureReadDir
+	NFSProcedureReadDirPlus
+	NFSProcedureFSStat
+	NFSProcedureFSInfo
+	NFSProcedurePathConf
+	NFSProcedureCommit
+)
+
+func (n NFSProcedure) String() string {
+	switch n {
+	case NFSProcedureNull:
+		return "Null"
+	case NFSProcedureGetAttr:
+		return "GetAttr"
+	case NFSProcedureSetAttr:
+		return "SetAttr"
+	case NFSProcedureLookup:
+		return "Lookup"
+	case NFSProcedureAccess:
+		return "Access"
+	case NFSProcedureReadlink:
+		return "ReadLink"
+	case NFSProcedureRead:
+		return "Read"
+	case NFSProcedureWrite:
+		return "Write"
+	case NFSProcedureCreate:
+		return "Create"
+	case NFSProcedureMkDir:
+		return "Mkdir"
+	case NFSProcedureSymlink:
+		return "Symlink"
+	case NFSProcedureMkNod:
+		return "Mknod"
+	case NFSProcedureRemove:
+		return "Remove"
+	case NFSProcedureRmDir:
+		return "Rmdir"
+	case NFSProcedureRename:
+		return "Rename"
+	case NFSProcedureLink:
+		return "Link"
+	case NFSProcedureReadDir:
+		return "ReadDir"
+	case NFSProcedureReadDirPlus:
+		return "ReadDirPlus"
+	case NFSProcedureFSStat:
+		return "FSStat"
+	case NFSProcedureFSInfo:
+		return "FSInfo"
+	case NFSProcedurePathConf:
+		return "PathConf"
+	case NFSProcedureCommit:
+		return "Commit"
+	default:
+		return "Unknown"
+	}
+}
+
+// NFSStatus (nfsstat3) is a result code for nfs rpc calls
+type NFSStatus uint32
+
+// NFSStatus codes
+const (
+	NFSStatusOk          NFSStatus = 0
+	NFSStatusPerm        NFSStatus = 1
+	NFSStatusNoEnt       NFSStatus = 2
+	NFSStatusIO          NFSStatus = 5
+	NFSStatusNXIO        NFSStatus = 6
+	NFSStatusAccess      NFSStatus = 13
+	NFSStatusExist       NFSStatus = 17
+	NFSStatusXDev        NFSStatus = 18
+	NFSStatusNoDev       NFSStatus = 19
+	NFSStatusNotDir      NFSStatus = 20
+	NFSStatusIsDir       NFSStatus = 21
+	NFSStatusInval       NFSStatus = 22
+	NFSStatusFBig        NFSStatus = 27
+	NFSStatusNoSPC       NFSStatus = 28
+	NFSStatusROFS        NFSStatus = 30
+	NFSStatusMlink       NFSStatus = 31
+	NFSStatusNameTooLong NFSStatus = 63
+	NFSStatusNotEmpty    NFSStatus = 66
+	NFSStatusDQuot       NFSStatus = 69
+	NFSStatusStale       NFSStatus = 70
+	NFSStatusRemote      NFSStatus = 71
+	NFSStatusBadHandle   NFSStatus = 10001
+	NFSStatusNotSync     NFSStatus = 10002
+	NFSStatusBadCookie   NFSStatus = 10003
+	NFSStatusNotSupp     NFSStatus = 10004
+	NFSStatusTooSmall    NFSStatus = 10005
+	NFSStatusServerFault NFSStatus = 10006
+	NFSStatusBadType     NFSStatus = 10007
+	NFSStatusJukebox     NFSStatus = 10008
+)
+
+func (s NFSStatus) String() string {
+	switch s {
+	case NFSStatusOk:
+		return "Call Completed Successfull"
+	case NFSStatusPerm:
+		return "Not Owner"
+	case NFSStatusNoEnt:
+		return "No such file or directory"
+	case NFSStatusIO:
+		return "I/O error"
+	case NFSStatusNXIO:
+		return "I/O error: No such device"
+	case NFSStatusAccess:
+		return "Permission denied"
+	case NFSStatusExist:
+		return "File exists"
+	case NFSStatusXDev:
+		return "Attempt to do a cross device hard link"
+	case NFSStatusNoDev:
+		return "No such device"
+	case NFSStatusNotDir:
+		return "Not a directory"
+	case NFSStatusIsDir:
+		return "Is a directory"
+	case NFSStatusInval:
+		return "Invalid argument"
+	case NFSStatusFBig:
+		return "File too large"
+	case NFSStatusNoSPC:
+		return "No space left on device"
+	case NFSStatusROFS:
+		return "Read only file system"
+	case NFSStatusMlink:
+		return "Too many hard links"
+	case NFSStatusNameTooLong:
+		return "Name too long"
+	case NFSStatusNotEmpty:
+		return "Not empty"
+	case NFSStatusDQuot:
+		return "Resource quota exceeded"
+	case NFSStatusStale:
+		return "Invalid file handle"
+	case NFSStatusRemote:
+		return "Too many levels of remote in path"
+	case NFSStatusBadHandle:
+		return "Illegal NFS file handle"
+	case NFSStatusNotSync:
+		return "Synchronization mismatch"
+	case NFSStatusBadCookie:
+		return "Cookie is Stale"
+	case NFSStatusNotSupp:
+		return "Operation not supported"
+	case NFSStatusTooSmall:
+		return "Buffer or request too small"
+	case NFSStatusServerFault:
+		return "Unmapped error (EIO)"
+	case NFSStatusBadType:
+		return "Type not supported"
+	case NFSStatusJukebox:
+		return "Initiated, but too slow. Try again with new txn"
+	default:
+		return "unknown"
+	}
+}
+
+// DirOpArg is a common serialization used for referencing an object in a directory
+type DirOpArg struct {
+	Handle   []byte
+	Filename []byte
+}
diff --git a/pkg/go-nfs/server.go b/pkg/go-nfs/server.go
new file mode 100644
index 0000000..e9d2085
--- /dev/null
+++ b/pkg/go-nfs/server.go
@@ -0,0 +1,102 @@
+package nfs
+
+import (
+	"bytes"
+	"context"
+	"crypto/rand"
+	"errors"
+	"net"
+	"time"
+)
+
+// Server is a handle to the listening NFS server.
+type Server struct {
+	Handler
+	ID [8]byte
+}
+
+// RegisterMessageHandler registers a handler for a specific
+// XDR procedure.
+func RegisterMessageHandler(protocol uint32, proc uint32, handler HandleFunc) error {
+	if registeredHandlers == nil {
+		registeredHandlers = make(map[registeredHandlerID]HandleFunc)
+	}
+	for k := range registeredHandlers {
+		if k.protocol == protocol && k.proc == proc {
+			return errors.New("already registered")
+		}
+	}
+	id := registeredHandlerID{protocol, proc}
+	registeredHandlers[id] = handler
+	return nil
+}
+
+// HandleFunc represents a handler for a specific protocol message.
+type HandleFunc func(ctx context.Context, w *response, userHandler Handler) error
+
+// TODO: store directly as a uint64 for more efficient lookups
+type registeredHandlerID struct {
+	protocol uint32
+	proc     uint32
+}
+
+var registeredHandlers map[registeredHandlerID]HandleFunc
+
+// Serve listens on the provided listener port for incoming client requests.
+func (s *Server) Serve(l net.Listener) error {
+	defer l.Close()
+	if bytes.Equal(s.ID[:], []byte{0, 0, 0, 0, 0, 0, 0, 0}) {
+		if _, err := rand.Reader.Read(s.ID[:]); err != nil {
+			return err
+		}
+	}
+
+	var tempDelay time.Duration
+
+	for {
+		conn, err := l.Accept()
+		if err != nil {
+			if ne, ok := err.(net.Error); ok && ne.Timeout() {
+				if tempDelay == 0 {
+					tempDelay = 5 * time.Millisecond
+				} else {
+					tempDelay *= 2
+				}
+				if max := 1 * time.Second; tempDelay > max {
+					tempDelay = max
+				}
+				time.Sleep(tempDelay)
+				continue
+			}
+			return err
+		}
+		tempDelay = 0
+		c := s.newConn(conn)
+		go c.serve()
+	}
+}
+
+func (s *Server) newConn(nc net.Conn) *conn {
+	c := &conn{
+		Server: s,
+		Conn:   nc,
+	}
+	return c
+}
+
+// TODO: keep an immutable map for each server instance to have less
+// chance of races.
+func (s *Server) handlerFor(prog uint32, proc uint32) HandleFunc {
+	for k, v := range registeredHandlers {
+		if k.protocol == prog && k.proc == proc {
+			return v
+		}
+	}
+	return nil
+}
+
+// Serve is a singleton listener paralleling http.Serve
+func Serve(l net.Listener, handler Handler) error {
+	srv := &Server{Handler: handler}
+	return srv.Serve(l)
+}
diff --git a/pkg/go-nfs/time.go b/pkg/go-nfs/time.go
new file mode 100644
index 0000000..266dd27
--- /dev/null
+++ b/pkg/go-nfs/time.go
@@ -0,0 +1,32 @@
+package nfs
+
+import (
+	"time"
+)
+
+// FileTime is the NFS wire time format
+// This is equivalent to go-nfs-client/nfs.NFS3Time
+type FileTime struct {
+	Seconds  uint32
+	Nseconds uint32
+}
+
+// ToNFSTime generates the nfs 64bit time format from a golang time.
+func ToNFSTime(t time.Time) FileTime {
+	return FileTime{
+		Seconds:  uint32(t.Unix()),
+		Nseconds: uint32(t.UnixNano() % int64(time.Second)),
+	}
+}
+
+// Native generates a golang time from an nfs time spec
+func (t FileTime) Native() *time.Time {
+	ts := time.Unix(int64(t.Seconds), int64(t.Nseconds))
+	return &ts
+}
+
+// EqualTimespec returns if this time is equal to a local time spec
+func (t FileTime) EqualTimespec(sec int64, nsec int64) bool {
+	// TODO: bounds check on sec/nsec overflow
+	return t.Nseconds == uint32(nsec) && t.Seconds == uint32(sec)
+}
diff --git a/pkg/kvtrace/kvmetrics.go b/pkg/kvtrace/kvmetrics.go
new file mode 100644
index 0000000..869c37a
--- /dev/null
+++ b/pkg/kvtrace/kvmetrics.go
@@ -0,0 +1,90 @@
+package kvtrace
+
+import (
+	"context"
+
+	"github.com/royalcat/kv"
+	"go.opentelemetry.io/otel"
+	"go.opentelemetry.io/otel/attribute"
+	"go.opentelemetry.io/otel/trace"
+)
+
+var tracer = otel.Tracer("github.com/royalcat/kv/tracer")
+
+type traceSrtore[K, V any] struct {
+	kv    kv.Store[K, V]
+	attrs []attribute.KeyValue
+}
+
+func WrapTracing[K, V any](kv kv.Store[K, V], attrs ...attribute.KeyValue) kv.Store[K, V] {
+	return &traceSrtore[K, V]{
+		kv:    kv,
+		attrs: attrs,
+	}
+}
+
+// Close implements kv.Store.
+func (m *traceSrtore[K, V]) Close(ctx context.Context) error {
+	ctx, span := tracer.Start(ctx, "Close", trace.WithAttributes(m.attrs...))
+	defer span.End()
+
+	return m.kv.Close(ctx)
+}
+
+// Delete implements kv.Store.
+func (m *traceSrtore[K, V]) Delete(ctx context.Context, k K) error {
+	ctx, span := tracer.Start(ctx, "Delete", trace.WithAttributes(m.attrs...))
+	defer span.End()
+
+	return m.kv.Delete(ctx, k)
+}
+
+// Get implements kv.Store.
+func (m *traceSrtore[K, V]) Get(ctx context.Context, k K) (v V, found bool, err error) {
+	ctx, span := tracer.Start(ctx, "Get", trace.WithAttributes(m.attrs...))
+	defer span.End()
+
+	return m.kv.Get(ctx, k)
+}
+
+// Range implements kv.Store.
+func (m *traceSrtore[K, V]) Range(ctx context.Context, iter kv.Iter[K, V]) error {
+	ctx, span := tracer.Start(ctx, "Range", trace.WithAttributes(m.attrs...))
+	defer span.End()
+
+	count := 0
+	iterCount := func(k K, v V) bool {
+		count++
+		return iter(k, v)
+	}
+
+	err := m.kv.Range(ctx, iterCount)
+	span.SetAttributes(attribute.Int("count", count))
+	return err
+}
+
+// RangeWithPrefix implements kv.Store.
+func (m *traceSrtore[K, V]) RangeWithPrefix(ctx context.Context, k K, iter kv.Iter[K, V]) error {
+	ctx, span := tracer.Start(ctx, "RangeWithPrefix", trace.WithAttributes(m.attrs...))
+	defer span.End()
+
+	count := 0
+	iterCount := func(k K, v V) bool {
+		count++
+		return iter(k, v)
+	}
+
+	err := m.kv.Range(ctx, iterCount)
+	span.SetAttributes(attribute.Int("count", count))
+	return err
+}
+
+// Set implements kv.Store.
+func (m *traceSrtore[K, V]) Set(ctx context.Context, k K, v V) error {
+	ctx, span := tracer.Start(ctx, "Set", trace.WithAttributes(m.attrs...))
+	defer span.End()
+
+	return m.kv.Set(ctx, k, v)
+}
+
+var _ kv.Store[any, any] = (*traceSrtore[any, any])(nil)
diff --git a/src/config/default.go b/src/config/default.go
index e75be06..5ace89f 100644
--- a/src/config/default.go
+++ b/src/config/default.go
@@ -1,6 +1,6 @@
 package config
 
-var defaultConfig = Config{
+var defaultConfig = Settings{
 	SourceDir: "./data",
 	WebUi: WebUi{
 		Port: 4444,
@@ -21,8 +21,9 @@ var defaultConfig = Config{
 			Enabled: false,
 		},
 		NFS: NFS{
-			Enabled: false,
-			Port:    8122,
+			Enabled:   false,
+			Port:      8122,
+			CachePath: "./nfs-cache",
 		},
 	},
 
diff --git a/src/config/load.go b/src/config/load.go
index 2694baa..53f1156 100644
--- a/src/config/load.go
+++ b/src/config/load.go
@@ -13,8 +13,9 @@ import (
 
 var k = koanf.New(".")
 
-func Load(path string) (*Config, error) {
+var Config = defaultConfig
 
+func Load(path string) (*Settings, error) {
 	err := k.Load(structs.Provider(defaultConfig, "koanf"), nil)
 	if err != nil {
 		return nil, err
@@ -50,7 +51,7 @@ func Load(path string) (*Config, error) {
 		return nil, err
 	}
 
-	conf := Config{}
+	conf := Settings{}
 	err = k.Unmarshal("", &conf)
 	if err != nil {
 		return nil, err
diff --git a/src/config/model.go b/src/config/model.go
index d8e8c03..8dd744e 100644
--- a/src/config/model.go
+++ b/src/config/model.go
@@ -1,7 +1,7 @@
 package config
 
 // Config is the main config object
-type Config struct {
+type Settings struct {
 	WebUi         WebUi         `koanf:"webUi"`
 	TorrentClient TorrentClient `koanf:"torrent"`
 	Mounts        Mounts        `koanf:"mounts"`
@@ -67,8 +67,9 @@ type Mounts struct {
 }
 
 type NFS struct {
-	Enabled bool `koanf:"enabled"`
-	Port    int  `koanf:"port"`
+	Enabled   bool   `koanf:"enabled"`
+	Port      int    `koanf:"port"`
+	CachePath string `koanf:"cache_path"`
 }
 
 type HttpFs struct {
diff --git a/src/delivery/graphql/generated.go b/src/delivery/graphql/generated.go
index 2785488..3253c0c 100644
--- a/src/delivery/graphql/generated.go
+++ b/src/delivery/graphql/generated.go
@@ -57,6 +57,11 @@ type ComplexityRoot struct {
 		Size func(childComplexity int) int
 	}
 
+	CleanupResponse struct {
+		Count func(childComplexity int) int
+		List  func(childComplexity int) int
+	}
+
 	Dir struct {
 		Name func(childComplexity int) int
 	}
@@ -70,8 +75,14 @@ type ComplexityRoot struct {
 		Size func(childComplexity int) int
 	}
 
+	ListDirResponse struct {
+		Entries func(childComplexity int) int
+		Root    func(childComplexity int) int
+	}
+
 	Mutation struct {
 		CleanupTorrents  func(childComplexity int, files *bool, dryRun bool) int
+		DedupeStorage    func(childComplexity int) int
 		DownloadTorrent  func(childComplexity int, infohash string, file *string) int
 		ValidateTorrents func(childComplexity int, filter model.TorrentFilter) int
 	}
@@ -138,12 +149,13 @@ type ComplexityRoot struct {
 
 type MutationResolver interface {
 	ValidateTorrents(ctx context.Context, filter model.TorrentFilter) (bool, error)
-	CleanupTorrents(ctx context.Context, files *bool, dryRun bool) (int64, error)
+	CleanupTorrents(ctx context.Context, files *bool, dryRun bool) (*model.CleanupResponse, error)
 	DownloadTorrent(ctx context.Context, infohash string, file *string) (*model.DownloadTorrentResponse, error)
+	DedupeStorage(ctx context.Context) (int64, error)
 }
 type QueryResolver interface {
 	Torrents(ctx context.Context, filter *model.TorrentsFilter, pagination *model.Pagination) ([]*model.Torrent, error)
-	FsListDir(ctx context.Context, path string) ([]model.DirEntry, error)
+	FsListDir(ctx context.Context, path string) (*model.ListDirResponse, error)
 }
 type SubscriptionResolver interface {
 	TaskProgress(ctx context.Context, taskID string) (<-chan model.Progress, error)
@@ -190,6 +202,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.ArchiveFS.Size(childComplexity), true
 
+	case "CleanupResponse.count":
+		if e.complexity.CleanupResponse.Count == nil {
+			break
+		}
+
+		return e.complexity.CleanupResponse.Count(childComplexity), true
+
+	case "CleanupResponse.list":
+		if e.complexity.CleanupResponse.List == nil {
+			break
+		}
+
+		return e.complexity.CleanupResponse.List(childComplexity), true
+
 	case "Dir.name":
 		if e.complexity.Dir.Name == nil {
 			break
@@ -218,6 +244,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.File.Size(childComplexity), true
 
+	case "ListDirResponse.entries":
+		if e.complexity.ListDirResponse.Entries == nil {
+			break
+		}
+
+		return e.complexity.ListDirResponse.Entries(childComplexity), true
+
+	case "ListDirResponse.root":
+		if e.complexity.ListDirResponse.Root == nil {
+			break
+		}
+
+		return e.complexity.ListDirResponse.Root(childComplexity), true
+
 	case "Mutation.cleanupTorrents":
 		if e.complexity.Mutation.CleanupTorrents == nil {
 			break
@@ -230,6 +270,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.Mutation.CleanupTorrents(childComplexity, args["files"].(*bool), args["dryRun"].(bool)), true
 
+	case "Mutation.dedupeStorage":
+		if e.complexity.Mutation.DedupeStorage == nil {
+			break
+		}
+
+		return e.complexity.Mutation.DedupeStorage(childComplexity), true
+
 	case "Mutation.downloadTorrent":
 		if e.complexity.Mutation.DownloadTorrent == nil {
 			break
@@ -602,28 +649,34 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er
 
 var sources = []*ast.Source{
 	{Name: "../../../graphql/mutation.graphql", Input: `type Mutation {
-    validateTorrents(filter: TorrentFilter!): Boolean!
-    cleanupTorrents(files: Boolean, dryRun: Boolean!): Int!
-    downloadTorrent(infohash: String!, file: String): DownloadTorrentResponse
+  validateTorrents(filter: TorrentFilter!): Boolean!
+  cleanupTorrents(files: Boolean, dryRun: Boolean!): CleanupResponse!
+  downloadTorrent(infohash: String!, file: String): DownloadTorrentResponse
+  dedupeStorage: Int!
 }
 
-
 input TorrentFilter @oneOf {
-    everything: Boolean
-    infohash: String
-    # pathGlob: String!
+  everything: Boolean
+  infohash: String
+  # pathGlob: String!
 }
 
 type DownloadTorrentResponse {
-    task: Task
+  task: Task
+}
+
+type CleanupResponse {
+  count: Int!
+  list: [String!]!
 }
 
 type Task {
-    id: ID!
-}`, BuiltIn: false},
+  id: ID!
+}
+`, BuiltIn: false},
 	{Name: "../../../graphql/query.graphql", Input: `type Query {
   torrents(filter: TorrentsFilter, pagination: Pagination): [Torrent!]!
-  fsListDir(path: String!): [DirEntry!]!
+  fsListDir(path: String!): ListDirResponse!
 }
 
 input TorrentsFilter {
@@ -634,6 +687,11 @@ input TorrentsFilter {
   peersCount: IntFilter
 }
 
+type ListDirResponse {
+  root: DirEntry!
+  entries: [DirEntry!]!
+}
+
 input Pagination {
   offset: Int!
   limit: Int!
@@ -1008,6 +1066,94 @@ func (ec *executionContext) fieldContext_ArchiveFS_size(ctx context.Context, fie
 	return fc, nil
 }
 
+func (ec *executionContext) _CleanupResponse_count(ctx context.Context, field graphql.CollectedField, obj *model.CleanupResponse) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_CleanupResponse_count(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Count, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(int64)
+	fc.Result = res
+	return ec.marshalNInt2int64(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_CleanupResponse_count(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "CleanupResponse",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Int does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _CleanupResponse_list(ctx context.Context, field graphql.CollectedField, obj *model.CleanupResponse) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_CleanupResponse_list(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.List, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.([]string)
+	fc.Result = res
+	return ec.marshalNString2ᚕstringᚄ(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_CleanupResponse_list(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "CleanupResponse",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type String does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
 func (ec *executionContext) _Dir_name(ctx context.Context, field graphql.CollectedField, obj *model.Dir) (ret graphql.Marshaler) {
 	fc, err := ec.fieldContext_Dir_name(ctx, field)
 	if err != nil {
@@ -1185,6 +1331,94 @@ func (ec *executionContext) fieldContext_File_size(ctx context.Context, field gr
 	return fc, nil
 }
 
+func (ec *executionContext) _ListDirResponse_root(ctx context.Context, field graphql.CollectedField, obj *model.ListDirResponse) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_ListDirResponse_root(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Root, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(model.DirEntry)
+	fc.Result = res
+	return ec.marshalNDirEntry2gitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐDirEntry(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_ListDirResponse_root(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "ListDirResponse",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("FieldContext.Child cannot be called on type INTERFACE")
+		},
+	}
+	return fc, nil
+}
+
+func (ec *executionContext) _ListDirResponse_entries(ctx context.Context, field graphql.CollectedField, obj *model.ListDirResponse) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_ListDirResponse_entries(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Entries, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.([]model.DirEntry)
+	fc.Result = res
+	return ec.marshalNDirEntry2ᚕgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐDirEntryᚄ(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_ListDirResponse_entries(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "ListDirResponse",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("FieldContext.Child cannot be called on type INTERFACE")
+		},
+	}
+	return fc, nil
+}
+
 func (ec *executionContext) _Mutation_validateTorrents(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
 	fc, err := ec.fieldContext_Mutation_validateTorrents(ctx, field)
 	if err != nil {
@@ -1266,9 +1500,9 @@ func (ec *executionContext) _Mutation_cleanupTorrents(ctx context.Context, field
 		}
 		return graphql.Null
 	}
-	res := resTmp.(int64)
+	res := resTmp.(*model.CleanupResponse)
 	fc.Result = res
-	return ec.marshalNInt2int64(ctx, field.Selections, res)
+	return ec.marshalNCleanupResponse2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐCleanupResponse(ctx, field.Selections, res)
 }
 
 func (ec *executionContext) fieldContext_Mutation_cleanupTorrents(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
@@ -1278,7 +1512,13 @@ func (ec *executionContext) fieldContext_Mutation_cleanupTorrents(ctx context.Co
 		IsMethod:   true,
 		IsResolver: true,
 		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
-			return nil, errors.New("field of type Int does not have child fields")
+			switch field.Name {
+			case "count":
+				return ec.fieldContext_CleanupResponse_count(ctx, field)
+			case "list":
+				return ec.fieldContext_CleanupResponse_list(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type CleanupResponse", field.Name)
 		},
 	}
 	defer func() {
@@ -1351,6 +1591,50 @@ func (ec *executionContext) fieldContext_Mutation_downloadTorrent(ctx context.Co
 	return fc, nil
 }
 
+func (ec *executionContext) _Mutation_dedupeStorage(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_Mutation_dedupeStorage(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return ec.resolvers.Mutation().DedupeStorage(rctx)
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(int64)
+	fc.Result = res
+	return ec.marshalNInt2int64(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Mutation_dedupeStorage(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "Mutation",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: true,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Int does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
 func (ec *executionContext) _Query_torrents(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
 	fc, err := ec.fieldContext_Query_torrents(ctx, field)
 	if err != nil {
@@ -1450,9 +1734,9 @@ func (ec *executionContext) _Query_fsListDir(ctx context.Context, field graphql.
 		}
 		return graphql.Null
 	}
-	res := resTmp.([]model.DirEntry)
+	res := resTmp.(*model.ListDirResponse)
 	fc.Result = res
-	return ec.marshalNDirEntry2ᚕgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐDirEntryᚄ(ctx, field.Selections, res)
+	return ec.marshalNListDirResponse2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐListDirResponse(ctx, field.Selections, res)
 }
 
 func (ec *executionContext) fieldContext_Query_fsListDir(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
@@ -1462,7 +1746,13 @@ func (ec *executionContext) fieldContext_Query_fsListDir(ctx context.Context, fi
 		IsMethod:   true,
 		IsResolver: true,
 		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
-			return nil, errors.New("FieldContext.Child cannot be called on type INTERFACE")
+			switch field.Name {
+			case "root":
+				return ec.fieldContext_ListDirResponse_root(ctx, field)
+			case "entries":
+				return ec.fieldContext_ListDirResponse_entries(ctx, field)
+			}
+			return nil, fmt.Errorf("no field named %q was found under type ListDirResponse", field.Name)
 		},
 	}
 	defer func() {
@@ -1723,6 +2013,8 @@ func (ec *executionContext) fieldContext_Schema_mutation(ctx context.Context, fi
 				return ec.fieldContext_Mutation_cleanupTorrents(ctx, field)
 			case "downloadTorrent":
 				return ec.fieldContext_Mutation_downloadTorrent(ctx, field)
+			case "dedupeStorage":
+				return ec.fieldContext_Mutation_dedupeStorage(ctx, field)
 			}
 			return nil, fmt.Errorf("no field named %q was found under type Mutation", field.Name)
 		},
@@ -5400,6 +5692,50 @@ func (ec *executionContext) _ArchiveFS(ctx context.Context, sel ast.SelectionSet
 	return out
 }
 
+var cleanupResponseImplementors = []string{"CleanupResponse"}
+
+func (ec *executionContext) _CleanupResponse(ctx context.Context, sel ast.SelectionSet, obj *model.CleanupResponse) graphql.Marshaler {
+	fields := graphql.CollectFields(ec.OperationContext, sel, cleanupResponseImplementors)
+
+	out := graphql.NewFieldSet(fields)
+	deferred := make(map[string]*graphql.FieldSet)
+	for i, field := range fields {
+		switch field.Name {
+		case "__typename":
+			out.Values[i] = graphql.MarshalString("CleanupResponse")
+		case "count":
+			out.Values[i] = ec._CleanupResponse_count(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "list":
+			out.Values[i] = ec._CleanupResponse_list(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		default:
+			panic("unknown field " + strconv.Quote(field.Name))
+		}
+	}
+	out.Dispatch(ctx)
+	if out.Invalids > 0 {
+		return graphql.Null
+	}
+
+	atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+	for label, dfs := range deferred {
+		ec.processDeferredGroup(graphql.DeferredGroup{
+			Label:    label,
+			Path:     graphql.GetPath(ctx),
+			FieldSet: dfs,
+			Context:  ctx,
+		})
+	}
+
+	return out
+}
+
 var dirImplementors = []string{"Dir", "DirEntry"}
 
 func (ec *executionContext) _Dir(ctx context.Context, sel ast.SelectionSet, obj *model.Dir) graphql.Marshaler {
@@ -5519,6 +5855,50 @@ func (ec *executionContext) _File(ctx context.Context, sel ast.SelectionSet, obj
 	return out
 }
 
+var listDirResponseImplementors = []string{"ListDirResponse"}
+
+func (ec *executionContext) _ListDirResponse(ctx context.Context, sel ast.SelectionSet, obj *model.ListDirResponse) graphql.Marshaler {
+	fields := graphql.CollectFields(ec.OperationContext, sel, listDirResponseImplementors)
+
+	out := graphql.NewFieldSet(fields)
+	deferred := make(map[string]*graphql.FieldSet)
+	for i, field := range fields {
+		switch field.Name {
+		case "__typename":
+			out.Values[i] = graphql.MarshalString("ListDirResponse")
+		case "root":
+			out.Values[i] = ec._ListDirResponse_root(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		case "entries":
+			out.Values[i] = ec._ListDirResponse_entries(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
+		default:
+			panic("unknown field " + strconv.Quote(field.Name))
+		}
+	}
+	out.Dispatch(ctx)
+	if out.Invalids > 0 {
+		return graphql.Null
+	}
+
+	atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+	for label, dfs := range deferred {
+		ec.processDeferredGroup(graphql.DeferredGroup{
+			Label:    label,
+			Path:     graphql.GetPath(ctx),
+			FieldSet: dfs,
+			Context:  ctx,
+		})
+	}
+
+	return out
+}
+
 var mutationImplementors = []string{"Mutation"}
 
 func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler {
@@ -5556,6 +5936,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
 			out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
 				return ec._Mutation_downloadTorrent(ctx, field)
 			})
+		case "dedupeStorage":
+			out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
+				return ec._Mutation_dedupeStorage(ctx, field)
+			})
+			if out.Values[i] == graphql.Null {
+				out.Invalids++
+			}
 		default:
 			panic("unknown field " + strconv.Quote(field.Name))
 		}
@@ -6551,6 +6938,20 @@ func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.Se
 	return res
 }
 
+func (ec *executionContext) marshalNCleanupResponse2gitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐCleanupResponse(ctx context.Context, sel ast.SelectionSet, v model.CleanupResponse) graphql.Marshaler {
+	return ec._CleanupResponse(ctx, sel, &v)
+}
+
+func (ec *executionContext) marshalNCleanupResponse2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐCleanupResponse(ctx context.Context, sel ast.SelectionSet, v *model.CleanupResponse) graphql.Marshaler {
+	if v == nil {
+		if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+			ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+		}
+		return graphql.Null
+	}
+	return ec._CleanupResponse(ctx, sel, v)
+}
+
 func (ec *executionContext) marshalNDirEntry2gitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐDirEntry(ctx context.Context, sel ast.SelectionSet, v model.DirEntry) graphql.Marshaler {
 	if v == nil {
 		if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
@@ -6650,6 +7051,20 @@ func (ec *executionContext) marshalNInt2int64(ctx context.Context, sel ast.Selec
 	return res
 }
 
+func (ec *executionContext) marshalNListDirResponse2gitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐListDirResponse(ctx context.Context, sel ast.SelectionSet, v model.ListDirResponse) graphql.Marshaler {
+	return ec._ListDirResponse(ctx, sel, &v)
+}
+
+func (ec *executionContext) marshalNListDirResponse2ᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐListDirResponse(ctx context.Context, sel ast.SelectionSet, v *model.ListDirResponse) graphql.Marshaler {
+	if v == nil {
+		if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+			ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+		}
+		return graphql.Null
+	}
+	return ec._ListDirResponse(ctx, sel, v)
+}
+
 func (ec *executionContext) unmarshalNString2string(ctx context.Context, v interface{}) (string, error) {
 	res, err := graphql.UnmarshalString(v)
 	return res, graphql.ErrorOnPath(ctx, err)
@@ -6665,6 +7080,38 @@ func (ec *executionContext) marshalNString2string(ctx context.Context, sel ast.S
 	return res
 }
 
+func (ec *executionContext) unmarshalNString2ᚕstringᚄ(ctx context.Context, v interface{}) ([]string, error) {
+	var vSlice []interface{}
+	if v != nil {
+		vSlice = graphql.CoerceList(v)
+	}
+	var err error
+	res := make([]string, len(vSlice))
+	for i := range vSlice {
+		ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i))
+		res[i], err = ec.unmarshalNString2string(ctx, vSlice[i])
+		if err != nil {
+			return nil, err
+		}
+	}
+	return res, nil
+}
+
+func (ec *executionContext) marshalNString2ᚕstringᚄ(ctx context.Context, sel ast.SelectionSet, v []string) graphql.Marshaler {
+	ret := make(graphql.Array, len(v))
+	for i := range v {
+		ret[i] = ec.marshalNString2string(ctx, sel, v[i])
+	}
+
+	for _, e := range ret {
+		if e == graphql.Null {
+			return graphql.Null
+		}
+	}
+
+	return ret
+}
+
 func (ec *executionContext) marshalNTorrent2ᚕᚖgitᚗkmsignᚗruᚋroyalcatᚋtstorᚋsrcᚋdeliveryᚋgraphqlᚋmodelᚐTorrentᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.Torrent) graphql.Marshaler {
 	ret := make(graphql.Array, len(v))
 	var wg sync.WaitGroup
diff --git a/src/delivery/graphql/model/models_gen.go b/src/delivery/graphql/model/models_gen.go
index 4a56ee6..6852932 100644
--- a/src/delivery/graphql/model/models_gen.go
+++ b/src/delivery/graphql/model/models_gen.go
@@ -32,6 +32,11 @@ type BooleanFilter struct {
 	Eq *bool `json:"eq,omitempty"`
 }
 
+type CleanupResponse struct {
+	Count int64    `json:"count"`
+	List  []string `json:"list"`
+}
+
 type DateTimeFilter struct {
 	Eq  *time.Time `json:"eq,omitempty"`
 	Gt  *time.Time `json:"gt,omitempty"`
@@ -68,6 +73,11 @@ type IntFilter struct {
 	In  []int64 `json:"in,omitempty"`
 }
 
+type ListDirResponse struct {
+	Root    DirEntry   `json:"root"`
+	Entries []DirEntry `json:"entries"`
+}
+
 type Mutation struct {
 }
 
diff --git a/src/delivery/graphql/resolver/mutation.resolvers.go b/src/delivery/graphql/resolver/mutation.resolvers.go
index 210b7a6..ed7dc31 100644
--- a/src/delivery/graphql/resolver/mutation.resolvers.go
+++ b/src/delivery/graphql/resolver/mutation.resolvers.go
@@ -46,18 +46,24 @@ func (r *mutationResolver) ValidateTorrents(ctx context.Context, filter model.To
 }
 
 // CleanupTorrents is the resolver for the cleanupTorrents field.
-func (r *mutationResolver) CleanupTorrents(ctx context.Context, files *bool, dryRun bool) (int64, error) {
+func (r *mutationResolver) CleanupTorrents(ctx context.Context, files *bool, dryRun bool) (*model.CleanupResponse, error) {
 	torrents, err := r.Service.ListTorrents(ctx)
 	if err != nil {
-		return 0, err
+		return nil, err
 	}
 
 	if files != nil && *files {
 		r, err := r.Service.Storage.CleanupFiles(ctx, torrents, dryRun)
-		return int64(r), err
+		return &model.CleanupResponse{
+			Count: int64(len(r)),
+			List:  r,
+		}, err
 	} else {
 		r, err := r.Service.Storage.CleanupDirs(ctx, torrents, dryRun)
-		return int64(r), err
+		return &model.CleanupResponse{
+			Count: int64(len(r)),
+			List:  r,
+		}, err
 	}
 }
 
@@ -80,6 +86,15 @@ func (r *mutationResolver) DownloadTorrent(ctx context.Context, infohash string,
 	return &model.DownloadTorrentResponse{}, nil
 }
 
+// DedupeStorage is the resolver for the dedupeStorage field.
+func (r *mutationResolver) DedupeStorage(ctx context.Context) (int64, error) {
+	deduped, err := r.Service.Storage.Dedupe(ctx)
+	if err != nil {
+		return 0, err
+	}
+	return int64(deduped), nil
+}
+
 // Mutation returns graph.MutationResolver implementation.
 func (r *Resolver) Mutation() graph.MutationResolver { return &mutationResolver{r} }
 
diff --git a/src/delivery/graphql/resolver/query.resolvers.go b/src/delivery/graphql/resolver/query.resolvers.go
index a0a826d..1b6aa43 100644
--- a/src/delivery/graphql/resolver/query.resolvers.go
+++ b/src/delivery/graphql/resolver/query.resolvers.go
@@ -6,6 +6,7 @@ package resolver
 
 import (
 	"context"
+	"io/fs"
 
 	graph "git.kmsign.ru/royalcat/tstor/src/delivery/graphql"
 	"git.kmsign.ru/royalcat/tstor/src/delivery/graphql/model"
@@ -64,51 +65,75 @@ func (r *queryResolver) Torrents(ctx context.Context, filter *model.TorrentsFilt
 	return tr, nil
 }
 
+type dirEntry interface {
+	Name() string
+	IsDir() bool
+}
+
+func fillDirEntry(e dirEntry) model.DirEntry {
+	switch e.(type) {
+	case *vfs.ArchiveFS:
+		e := e.(*vfs.ArchiveFS)
+		return model.ArchiveFs{
+			Name: e.Name(),
+			Size: e.Size(),
+		}
+	case *vfs.ResolverFS:
+		e := e.(*vfs.ResolverFS)
+		return model.ResolverFs{
+			Name: e.Name(),
+		}
+	case *vfs.TorrentFs:
+		e := e.(*vfs.TorrentFs)
+		return model.TorrentFs{
+			Name:    e.Name(),
+			Torrent: model.MapTorrent(e.Torrent),
+		}
+	default:
+		if e.IsDir() {
+			return model.Dir{
+				Name: e.Name(),
+			}
+		}
+		if de, ok := e.(fs.DirEntry); ok {
+			info, _ := de.Info()
+			return model.File{
+				Name: e.Name(),
+				Size: info.Size(),
+			}
+		}
+
+		if fe, ok := e.(fs.FileInfo); ok {
+			return model.File{
+				Name: fe.Name(),
+				Size: fe.Size(),
+			}
+		}
+	}
+
+	panic("this dir entry is strange af")
+}
+
 // FsListDir is the resolver for the fsListDir field.
-func (r *queryResolver) FsListDir(ctx context.Context, path string) ([]model.DirEntry, error) {
+func (r *queryResolver) FsListDir(ctx context.Context, path string) (*model.ListDirResponse, error) {
+	root, err := r.VFS.Stat(ctx, path)
+	if err != nil {
+		return nil, err
+	}
+
 	entries, err := r.VFS.ReadDir(ctx, path)
 	if err != nil {
 		return nil, err
 	}
 	out := []model.DirEntry{}
 	for _, e := range entries {
-		switch e.(type) {
-		case *vfs.ArchiveFS:
-			e := e.(*vfs.ArchiveFS)
-			out = append(out, model.ArchiveFs{
-				Name: e.Name(),
-				Size: e.Size,
-			})
-		case *vfs.ResolverFS:
-			e := e.(*vfs.ResolverFS)
-			out = append(out, model.ResolverFs{
-				Name: e.Name(),
-			})
-		case *vfs.TorrentFs:
-			e := e.(*vfs.TorrentFs)
-			out = append(out, model.TorrentFs{
-				Name:    e.Name(),
-				Torrent: model.MapTorrent(e.Torrent),
-			})
-		default:
-			if e.IsDir() {
-				out = append(out, model.Dir{Name: e.Name()})
-			} else {
-				info, err := e.Info()
-				if err != nil {
-					return nil, err
-				}
-
-				out = append(out, model.File{
-					Name: e.Name(),
-					Size: info.Size(),
-				})
-			}
-		}
-
+		out = append(out, fillDirEntry(e))
 	}
 
-	return out, nil
+	return &model.ListDirResponse{
+		Root:    fillDirEntry(root),
+		Entries: out,
+	}, nil
 }
 
 // Query returns graph.QueryResolver implementation.
diff --git a/src/delivery/http.go b/src/delivery/http.go
index 244ec16..92dbc55 100644
--- a/src/delivery/http.go
+++ b/src/delivery/http.go
@@ -15,7 +15,7 @@ import (
 	"github.com/shurcooL/httpfs/html/vfstemplate"
 )
 
-func New(fc *filecache.Cache, ss *service.Stats, s *service.Service, vfs vfs.Filesystem, logPath string, cfg *config.Config) error {
+func New(fc *filecache.Cache, ss *service.Stats, s *service.Service, vfs vfs.Filesystem, logPath string, cfg *config.Settings) error {
 	log := slog.With()
 
 	gin.SetMode(gin.ReleaseMode)
diff --git a/src/delivery/router.go b/src/delivery/router.go
index d03b2ff..e9e108f 100644
--- a/src/delivery/router.go
+++ b/src/delivery/router.go
@@ -7,6 +7,7 @@ import (
 	"git.kmsign.ru/royalcat/tstor/src/delivery/graphql/resolver"
 	"git.kmsign.ru/royalcat/tstor/src/host/service"
 	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
+	"github.com/99designs/gqlgen/graphql"
 	"github.com/99designs/gqlgen/graphql/handler"
 	"github.com/99designs/gqlgen/graphql/handler/extension"
 	"github.com/99designs/gqlgen/graphql/handler/lru"
@@ -33,7 +34,11 @@ func GraphQLHandler(service *service.Service, vfs vfs.Filesystem) http.Handler {
 	graphqlHandler.SetQueryCache(lru.New(1000))
 	graphqlHandler.Use(extension.Introspection{})
 	graphqlHandler.Use(extension.AutomaticPersistedQuery{Cache: lru.New(100)})
-	graphqlHandler.Use(otelgqlgen.Middleware())
+	graphqlHandler.Use(otelgqlgen.Middleware(
+		otelgqlgen.WithCreateSpanFromFields(func(ctx *graphql.FieldContext) bool {
+			return ctx.Field.Directives.ForName("link") != nil
+		}),
+	))
 
 	return graphqlHandler
 }
diff --git a/src/export/httpfs/httpfs.go b/src/export/httpfs/httpfs.go
index 8a7c188..9a03a23 100644
--- a/src/export/httpfs/httpfs.go
+++ b/src/export/httpfs/httpfs.go
@@ -76,7 +76,7 @@ func (hfs *HTTPFS) filesToFileInfo(name string) ([]fs.FileInfo, error) {
 	return out, nil
 }
 
-var _ http.File = &httpFile{}
+var _ http.File = (*httpFile)(nil)
 
 type httpFile struct {
 	f vfs.File
@@ -128,5 +128,5 @@ func (f *httpFile) Readdir(count int) ([]fs.FileInfo, error) {
 }
 
 func (f *httpFile) Stat() (fs.FileInfo, error) {
-	return f.f.Stat()
+	return f.f.Info()
 }
diff --git a/src/export/nfs/handler.go b/src/export/nfs/handler.go
index 6baa241..18ef6bd 100644
--- a/src/export/nfs/handler.go
+++ b/src/export/nfs/handler.go
@@ -2,11 +2,12 @@ package nfs
 
 import (
 	"log/slog"
+	"time"
 
+	nfs "git.kmsign.ru/royalcat/tstor/pkg/go-nfs"
+	nfshelper "git.kmsign.ru/royalcat/tstor/pkg/go-nfs/helpers"
 	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
 	"git.kmsign.ru/royalcat/tstor/src/log"
-	nfs "github.com/willscott/go-nfs"
-	nfshelper "github.com/willscott/go-nfs/helpers"
 )
 
 func NewNFSv3Handler(fs vfs.Filesystem) (nfs.Handler, error) {
@@ -14,10 +15,13 @@ func NewNFSv3Handler(fs vfs.Filesystem) (nfs.Handler, error) {
 	nfs.SetLogger(log.NewNFSLog(nfslog))
 	nfs.Log.SetLevel(nfs.InfoLevel)
 
-	bfs := &billyFsWrapper{fs: fs, log: nfslog}
+	bfs := &fsWrapper{fs: fs, log: nfslog, timeout: time.Minute}
 	handler := nfshelper.NewNullAuthHandler(bfs)
 
-	cacheHelper := nfshelper.NewCachingHandler(handler, 1024)
+	cacheHelper, err := NewKvHandler(handler, bfs)
+	if err != nil {
+		return nil, err
+	}
 
 	//  cacheHelper := NewCachingHandler(handler)
 
diff --git a/src/export/nfs/kvhandler.go b/src/export/nfs/kvhandler.go
new file mode 100644
index 0000000..c814405
--- /dev/null
+++ b/src/export/nfs/kvhandler.go
@@ -0,0 +1,127 @@
+package nfs
+
+import (
+	"context"
+	"fmt"
+	"path"
+	"slices"
+	"time"
+
+	"git.kmsign.ru/royalcat/tstor/pkg/go-nfs"
+	"git.kmsign.ru/royalcat/tstor/src/config"
+
+	"github.com/google/uuid"
+	"github.com/royalcat/kv"
+)
+
+const lifetime = time.Hour * 24
+
+// NewKvHandler provides a basic to/from-file handle cache that can be tuned with a smaller cache of active directory listings.
+func NewKvHandler(h nfs.Handler, fs nfs.Filesystem) (nfs.Handler, error) {
+	activeHandles, err := kv.NewBadgerKVMarhsler[uuid.UUID, []string](path.Join(config.Config.Mounts.NFS.CachePath, "handlers"))
+	if err != nil {
+		return nil, err
+	}
+
+	// if s, ok := activeHandles.(kv.BadgerStore); ok {
+	// 	db := s.BadgerDB()
+	// enable with managed database
+	// 	go func() {
+	// 		for n := range time.NewTimer(lifetime / 2).C {
+	// 			db.SetDiscardTs(uint64(n.Add(-lifetime).Unix()))
+	// 		}
+	// 	}()
+	// }
+
+	return &CachingHandler{
+		Handler:       h,
+		fs:            fs,
+		activeHandles: activeHandles,
+	}, nil
+}
+
+// CachingHandler implements to/from handle via an LRU cache.
+type CachingHandler struct {
+	nfs.Handler
+
+	fs            nfs.Filesystem
+	activeHandles kv.Store[uuid.UUID, []string]
+}
+
+// ToHandle takes a file and represents it with an opaque handle to reference it.
+// In stateless nfs (when it's serving a unix fs) this can be the device + inode
+// but we can generalize with a stateful local cache of handed out IDs.
+func (c *CachingHandler) ToHandle(_ nfs.Filesystem, path []string) []byte {
+
+	ctx := context.Background()
+
+	var id uuid.UUID
+	c.activeHandles.Range(ctx, func(k uuid.UUID, v []string) bool {
+		if slices.Equal(path, v) {
+			id = k
+			return false
+		}
+		return true
+	})
+
+	if id != uuid.Nil {
+		return id[:]
+	}
+
+	id = uuid.New()
+
+	c.activeHandles.Set(ctx, id, path)
+
+	return id[:]
+}
+
+// FromHandle converts from an opaque handle to the file it represents
+func (c *CachingHandler) FromHandle(fh []byte) (nfs.Filesystem, []string, error) {
+	ctx := context.Background()
+
+	id, err := uuid.FromBytes(fh)
+	if err != nil {
+		return nil, []string{}, err
+	}
+
+	paths, found, err := c.activeHandles.Get(ctx, id)
+	if err != nil {
+		return nil, nil, fmt.Errorf("kv error: %w", err)
+	}
+
+	if found {
+		return c.fs, paths, nil
+	}
+
+	return nil, []string{}, &nfs.NFSStatusError{NFSStatus: nfs.NFSStatusStale}
+}
+
+func (c *CachingHandler) InvalidateHandle(fs nfs.Filesystem, handle []byte) error {
+	ctx := context.Background()
+	//Remove from cache
+	id, err := uuid.FromBytes(handle)
+	if err != nil {
+		return err
+	}
+	c.activeHandles.Delete(ctx, id)
+	return nil
+}
+
+const maxInt = int(^uint(0) >> 1)
+
+// HandleLimit exports how many file handles can be safely stored by this cache.
+func (c *CachingHandler) HandleLimit() int {
+	return maxInt
+}
+
+func hasPrefix(path, prefix []string) bool {
+	if len(prefix) > len(path) {
+		return false
+	}
+	for i, e := range prefix {
+		if path[i] != e {
+			return false
+		}
+	}
+	return true
+}
diff --git a/src/export/nfs/wrapper-v3.go b/src/export/nfs/wrapper.go
similarity index 52%
rename from src/export/nfs/wrapper-v3.go
rename to src/export/nfs/wrapper.go
index 9891deb..753a5f9 100644
--- a/src/export/nfs/wrapper-v3.go
+++ b/src/export/nfs/wrapper.go
@@ -6,47 +6,45 @@ import (
 	"io/fs"
 	"log/slog"
 	"path/filepath"
+	"time"
+
+	"git.kmsign.ru/royalcat/tstor/pkg/ctxbilly"
+	nfs "git.kmsign.ru/royalcat/tstor/pkg/go-nfs"
 
 	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
 	"github.com/go-git/go-billy/v5"
-	"go.opentelemetry.io/otel"
-	"go.opentelemetry.io/otel/attribute"
-	"go.opentelemetry.io/otel/trace"
 )
 
-var billyFsTracer = otel.Tracer("git.kmsign.ru/royalcat/tstor/src/export/nfs.billyFsWrapper")
-
-type billyFsWrapper struct {
+type fsWrapper struct {
 	fs  vfs.Filesystem
 	log *slog.Logger
+
+	timeout time.Duration
 }
 
-var _ billy.Filesystem = (*billyFsWrapper)(nil)
-var _ billy.Dir = (*billyFsWrapper)(nil)
+var _ nfs.Filesystem = (*fsWrapper)(nil)
 
-func (*billyFsWrapper) ctx() context.Context {
-	return context.Background()
-}
+// var _ ctxbilly.Dir = (*billyFsWrapper)(nil)
 
 // Chroot implements billy.Filesystem.
-func (*billyFsWrapper) Chroot(path string) (billy.Filesystem, error) {
+func (*fsWrapper) Chroot(path string) (nfs.Filesystem, error) {
 	return nil, billy.ErrNotSupported
 }
 
 // Create implements billy.Filesystem.
-func (*billyFsWrapper) Create(filename string) (billy.File, error) {
+func (*fsWrapper) Create(ctx context.Context, filename string) (nfs.File, error) {
 	return nil, billy.ErrNotSupported
 }
 
 // Join implements billy.Filesystem.
-func (*billyFsWrapper) Join(elem ...string) string {
+func (*fsWrapper) Join(elem ...string) string {
 	return filepath.Join(elem...)
 }
 
 // Lstat implements billy.Filesystem.
-func (fs *billyFsWrapper) Lstat(filename string) (fs.FileInfo, error) {
-	ctx, span := billyFsTracer.Start(fs.ctx(), "Lstat", trace.WithAttributes(attribute.String("filename", filename)))
-	defer span.End()
+func (fs *fsWrapper) Lstat(ctx context.Context, filename string) (fs.FileInfo, error) {
+	ctx, cancel := context.WithTimeout(ctx, fs.timeout)
+	defer cancel()
 
 	info, err := fs.fs.Stat(ctx, filename)
 	if err != nil {
@@ -56,16 +54,14 @@ func (fs *billyFsWrapper) Lstat(filename string) (fs.FileInfo, error) {
 }
 
 // MkdirAll implements billy.Filesystem.
-func (*billyFsWrapper) MkdirAll(filename string, perm fs.FileMode) error {
+func (*fsWrapper) MkdirAll(ctx context.Context, filename string, perm fs.FileMode) error {
 	return billy.ErrNotSupported
 }
 
 // Open implements billy.Filesystem.
-func (fs *billyFsWrapper) Open(filename string) (billy.File, error) {
-	ctx, span := billyFsTracer.Start(fs.ctx(), "Open",
-		trace.WithAttributes(attribute.String("filename", filename)),
-	)
-	defer span.End()
+func (fs *fsWrapper) Open(ctx context.Context, filename string) (nfs.File, error) {
+	ctx, cancel := context.WithTimeout(ctx, fs.timeout)
+	defer cancel()
 
 	file, err := fs.fs.Open(ctx, filename)
 	if err != nil {
@@ -79,11 +75,9 @@ func (fs *billyFsWrapper) Open(filename string) (billy.File, error) {
 }
 
 // OpenFile implements billy.Filesystem.
-func (fs *billyFsWrapper) OpenFile(filename string, flag int, perm fs.FileMode) (billy.File, error) {
-	ctx, span := billyFsTracer.Start(fs.ctx(), "OpenFile",
-		trace.WithAttributes(attribute.String("filename", filename)),
-	)
-	defer span.End()
+func (fs *fsWrapper) OpenFile(ctx context.Context, filename string, flag int, perm fs.FileMode) (nfs.File, error) {
+	ctx, cancel := context.WithTimeout(ctx, fs.timeout)
+	defer cancel()
 
 	file, err := fs.fs.Open(ctx, filename)
 	if err != nil {
@@ -97,11 +91,9 @@ func (fs *billyFsWrapper) OpenFile(filename string, flag int, perm fs.FileMode)
 }
 
 // ReadDir implements billy.Filesystem.
-func (bfs *billyFsWrapper) ReadDir(path string) ([]fs.FileInfo, error) {
-	ctx, span := billyFsTracer.Start(bfs.ctx(), "OpenFile",
-		trace.WithAttributes(attribute.String("path", path)),
-	)
-	defer span.End()
+func (bfs *fsWrapper) ReadDir(ctx context.Context, path string) ([]fs.FileInfo, error) {
+	ctx, cancel := context.WithTimeout(ctx, bfs.timeout)
+	defer cancel()
 
 	ffs, err := bfs.fs.ReadDir(ctx, path)
 	if err != nil {
@@ -125,36 +117,32 @@ func (bfs *billyFsWrapper) ReadDir(path string) ([]fs.FileInfo, error) {
 }
 
 // Readlink implements billy.Filesystem.
-func (*billyFsWrapper) Readlink(link string) (string, error) {
+func (*fsWrapper) Readlink(ctx context.Context, link string) (string, error) {
 	return "", billy.ErrNotSupported
 }
 
 // Remove implements billy.Filesystem.
-func (bfs *billyFsWrapper) Remove(filename string) error {
-	ctx, span := billyFsTracer.Start(bfs.ctx(), "Remove",
-		trace.WithAttributes(attribute.String("filename", filename)),
-	)
-	defer span.End()
+func (bfs *fsWrapper) Remove(ctx context.Context, filename string) error {
+	ctx, cancel := context.WithTimeout(ctx, bfs.timeout)
+	defer cancel()
 
 	return bfs.fs.Unlink(ctx, filename)
 }
 
 // Rename implements billy.Filesystem.
-func (*billyFsWrapper) Rename(oldpath string, newpath string) error {
+func (*fsWrapper) Rename(ctx context.Context, oldpath string, newpath string) error {
 	return billy.ErrNotSupported
 }
 
 // Root implements billy.Filesystem.
-func (*billyFsWrapper) Root() string {
+func (*fsWrapper) Root() string {
 	return "/"
 }
 
 // Stat implements billy.Filesystem.
-func (bfs *billyFsWrapper) Stat(filename string) (fs.FileInfo, error) {
-	ctx, span := billyFsTracer.Start(bfs.ctx(), "Remove",
-		trace.WithAttributes(attribute.String("filename", filename)),
-	)
-	defer span.End()
+func (bfs *fsWrapper) Stat(ctx context.Context, filename string) (fs.FileInfo, error) {
+	ctx, cancel := context.WithTimeout(ctx, bfs.timeout)
+	defer cancel()
 
 	info, err := bfs.fs.Stat(ctx, filename)
 	if err != nil {
@@ -164,28 +152,21 @@ func (bfs *billyFsWrapper) Stat(filename string) (fs.FileInfo, error) {
 }
 
 // Symlink implements billy.Filesystem.
-func (fs *billyFsWrapper) Symlink(target string, link string) error {
+func (fs *fsWrapper) Symlink(ctx context.Context, target string, link string) error {
 	return billyErr(nil, vfs.ErrNotImplemented, fs.log)
 }
 
-// TempFile implements billy.Filesystem.
-func (fs *billyFsWrapper) TempFile(dir string, prefix string) (billy.File, error) {
-	return nil, billyErr(nil, vfs.ErrNotImplemented, fs.log)
-}
-
 type billyFile struct {
-	ctx context.Context
-
 	name string
 	file vfs.File
 	log  *slog.Logger
 }
 
-var _ billy.File = (*billyFile)(nil)
+var _ ctxbilly.File = (*billyFile)(nil)
 
 // Close implements billy.File.
-func (f *billyFile) Close() error {
-	return f.Close()
+func (f *billyFile) Close(ctx context.Context) error {
+	return f.file.Close(ctx)
 }
 
 // Name implements billy.File.
@@ -194,31 +175,12 @@ func (f *billyFile) Name() string {
 }
 
 // Read implements billy.File.
-func (bf *billyFile) Read(p []byte) (n int, err error) {
-	ctx, span := billyFsTracer.Start(bf.ctx, "Read",
-		trace.WithAttributes(attribute.Int("length", len(p))),
-	)
-	defer func() {
-		span.SetAttributes(attribute.Int("read", n))
-		span.End()
-	}()
-
+func (bf *billyFile) Read(ctx context.Context, p []byte) (n int, err error) {
 	return bf.file.Read(ctx, p)
 }
 
 // ReadAt implements billy.File.
-func (bf *billyFile) ReadAt(p []byte, off int64) (n int, err error) {
-	ctx, span := billyFsTracer.Start(bf.ctx, "Read",
-		trace.WithAttributes(
-			attribute.Int("length", len(p)),
-			attribute.Int64("offset", off),
-		),
-	)
-	defer func() {
-		span.SetAttributes(attribute.Int("read", n))
-		span.End()
-	}()
-
+func (bf *billyFile) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) {
 	return bf.file.ReadAt(ctx, p, off)
 }
 
@@ -228,12 +190,12 @@ func (f *billyFile) Seek(offset int64, whence int) (int64, error) {
 }
 
 // Truncate implements billy.File.
-func (f *billyFile) Truncate(size int64) error {
+func (f *billyFile) Truncate(ctx context.Context, size int64) error {
 	return billyErr(nil, vfs.ErrNotImplemented, f.log)
 }
 
 // Write implements billy.File.
-func (f *billyFile) Write(p []byte) (n int, err error) {
+func (f *billyFile) Write(ctx context.Context, p []byte) (n int, err error) {
 	return 0, billyErr(nil, vfs.ErrNotImplemented, f.log)
 }
 
diff --git a/src/export/webdav/fs.go b/src/export/webdav/fs.go
index 1c8fc83..8b21345 100644
--- a/src/export/webdav/fs.go
+++ b/src/export/webdav/fs.go
@@ -96,6 +96,7 @@ type webDAVFile struct {
 func newFile(ctx context.Context, name string, f vfs.File, df func() ([]os.FileInfo, error)) *webDAVFile {
 	return &webDAVFile{
 		ctx:     ctx,
+		f:       f,
 		fi:      newFileInfo(name, f.Size(), f.IsDir()),
 		dirFunc: df,
 	}
diff --git a/src/host/datastorage/piece_storage.go b/src/host/datastorage/piece_storage.go
index 26091f8..5197e3b 100644
--- a/src/host/datastorage/piece_storage.go
+++ b/src/host/datastorage/piece_storage.go
@@ -13,13 +13,12 @@ import (
 	"github.com/anacrolix/torrent/storage"
 	"github.com/anacrolix/torrent/types/infohash"
 	"github.com/hashicorp/go-multierror"
-	"github.com/royalcat/kv"
 )
 
+// NOT USED
 type PieceStorage struct {
-	basePath    string
-	completion  storage.PieceCompletion
-	dirInfohash kv.Store[string, infohash.T]
+	basePath   string
+	completion storage.PieceCompletion
 }
 
 func NewPieceStorage(path string, completion storage.PieceCompletion) *PieceStorage {
@@ -29,8 +28,6 @@ func NewPieceStorage(path string, completion storage.PieceCompletion) *PieceStor
 	}
 }
 
-var _ DataStorage = (*PieceStorage)(nil)
-
 // OpenTorrent implements FileStorageDeleter.
 func (p *PieceStorage) OpenTorrent(info *metainfo.Info, infoHash infohash.T) (storage.TorrentImpl, error) {
 	torrentPath := path.Join(p.basePath, infoHash.HexString())
diff --git a/src/host/datastorage/setup.go b/src/host/datastorage/setup.go
index 5209385..209a5f3 100644
--- a/src/host/datastorage/setup.go
+++ b/src/host/datastorage/setup.go
@@ -10,7 +10,7 @@ import (
 	"github.com/anacrolix/torrent/storage"
 )
 
-func Setup(cfg config.TorrentClient) (DataStorage, storage.PieceCompletion, error) {
+func Setup(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)
diff --git a/src/host/datastorage/storage.go b/src/host/datastorage/storage.go
index c17768e..3587beb 100644
--- a/src/host/datastorage/storage.go
+++ b/src/host/datastorage/storage.go
@@ -2,49 +2,62 @@ package datastorage
 
 import (
 	"context"
+	"crypto/sha1"
+	"fmt"
+	"io"
+	"io/fs"
 	"log/slog"
 	"os"
 	"path"
 	"path/filepath"
 	"slices"
 
+	"git.kmsign.ru/royalcat/tstor/pkg/rlog"
 	"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)
-}
+// 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) *FileStorage {
-	return &FileStorage{
-		baseDir: baseDir,
+func NewFileStorage(baseDir string, pc storage.PieceCompletion) *DataStorage {
+	return &DataStorage{
 		ClientImplCloser: storage.NewFileOpts(storage.NewFileClientOpts{
 			ClientBaseDir:   baseDir,
 			PieceCompletion: pc,
 			TorrentDirMaker: torrentDir,
 			FilePathMaker:   filePath,
 		}),
+		baseDir:         baseDir,
 		pieceCompletion: pc,
 		log:             slog.With("component", "torrent-client"),
 	}
 }
 
 // File-based storage for torrents, that isn't yet bound to a particular torrent.
-type FileStorage struct {
+type DataStorage struct {
 	baseDir string
 	storage.ClientImplCloser
 	pieceCompletion storage.PieceCompletion
 	log             *slog.Logger
 }
 
-func (me *FileStorage) Close() error {
+func (me *DataStorage) Close() error {
 	return me.pieceCompletion.Close()
 }
 
@@ -61,14 +74,14 @@ func filePath(opts storage.FilePathMakerOpts) string {
 	return filepath.Join(opts.File.Path...)
 }
 
-func (fs *FileStorage) filePath(info *metainfo.Info, infoHash metainfo.Hash, fileInfo *metainfo.FileInfo) string {
+func (fs *DataStorage) filePath(info *metainfo.Info, infoHash metainfo.Hash, fileInfo *metainfo.FileInfo) string {
 	return filepath.Join(torrentDir(fs.baseDir, info, infoHash), filePath(storage.FilePathMakerOpts{
 		Info: info,
 		File: fileInfo,
 	}))
 }
 
-func (fs *FileStorage) DeleteFile(file *torrent.File) error {
+func (fs *DataStorage) DeleteFile(file *torrent.File) error {
 	info := file.Torrent().Info()
 	infoHash := file.Torrent().InfoHash()
 	torrentDir := torrentDir(fs.baseDir, info, infoHash)
@@ -88,7 +101,7 @@ func (fs *FileStorage) DeleteFile(file *torrent.File) error {
 	return os.Remove(filePath)
 }
 
-func (fs *FileStorage) CleanupDirs(ctx context.Context, expected []*controller.Torrent, dryRun bool) (int, error) {
+func (fs *DataStorage) CleanupDirs(ctx context.Context, expected []*controller.Torrent, dryRun bool) ([]string, error) {
 	log := fs.log.With("function", "CleanupDirs", "expectedTorrents", len(expected), "dryRun", dryRun)
 
 	expectedEntries := []string{}
@@ -98,7 +111,7 @@ func (fs *FileStorage) CleanupDirs(ctx context.Context, expected []*controller.T
 
 	entries, err := os.ReadDir(fs.baseDir)
 	if err != nil {
-		return 0, err
+		return nil, err
 	}
 
 	toDelete := []string{}
@@ -109,7 +122,7 @@ func (fs *FileStorage) CleanupDirs(ctx context.Context, expected []*controller.T
 	}
 
 	if ctx.Err() != nil {
-		return 0, ctx.Err()
+		return nil, ctx.Err()
 	}
 
 	log.Info("deleting trash data", "dirsCount", len(toDelete))
@@ -119,40 +132,34 @@ func (fs *FileStorage) CleanupDirs(ctx context.Context, expected []*controller.T
 			log.Warn("deleting trash data", "path", p)
 			err := os.RemoveAll(p)
 			if err != nil {
-				return i, err
+				return toDelete[:i], err
 			}
 		}
 	}
 
-	return len(toDelete), nil
+	return toDelete, nil
 }
 
-// func (fs *FileStorage) IsCompatable(ctx context.Context, addition *controller.Torrent, dryRun bool) (bool, error) {
-// 	log := fs.log.With("function", "IsCompatable", "addition", addition.Name())
-
-// 	ifp
-// }
-
-func (fs *FileStorage) CleanupFiles(ctx context.Context, expected []*controller.Torrent, dryRun bool) (int, error) {
-	log := fs.log.With("function", "CleanupFiles", "expectedTorrents", len(expected), "dryRun", dryRun)
+func (s *DataStorage) CleanupFiles(ctx context.Context, expected []*controller.Torrent, dryRun bool) ([]string, error) {
+	log := s.log.With("function", "CleanupFiles", "expectedTorrents", len(expected), "dryRun", dryRun)
 
 	expectedEntries := []string{}
 	{
 		for _, e := range expected {
 			files, err := e.Files(ctx)
 			if err != nil {
-				return 0, err
+				return nil, err
 			}
 
 			for _, f := range files {
-				expectedEntries = append(expectedEntries, fs.filePath(e.Torrent().Info(), e.Torrent().InfoHash(), ptr(f.FileInfo())))
+				expectedEntries = append(expectedEntries, s.filePath(e.Torrent().Info(), e.Torrent().InfoHash(), ptr(f.FileInfo())))
 			}
 		}
 	}
 
 	entries := []string{}
-	err := filepath.Walk(fs.baseDir,
-		func(path string, info os.FileInfo, err error) error {
+	err := filepath.WalkDir(s.baseDir,
+		func(path string, info fs.DirEntry, err error) error {
 			if err != nil {
 				return err
 			}
@@ -167,7 +174,7 @@ func (fs *FileStorage) CleanupFiles(ctx context.Context, expected []*controller.
 			return nil
 		})
 	if err != nil {
-		return 0, err
+		return nil, err
 	}
 
 	toDelete := []string{}
@@ -178,20 +185,243 @@ func (fs *FileStorage) CleanupFiles(ctx context.Context, expected []*controller.
 	}
 
 	if ctx.Err() != nil {
-		return len(toDelete), ctx.Err()
+		return toDelete, ctx.Err()
 	}
 
 	log.Info("deleting trash data", "filesCount", len(toDelete))
 	if !dryRun {
 		for i, p := range toDelete {
-			fs.log.Warn("deleting trash data", "path", p)
+			s.log.Warn("deleting trash data", "path", p)
 			err := os.Remove(p)
 			if err != nil {
-				return i, err
+				return toDelete[i:], err
 			}
 		}
 	}
-	return len(toDelete), nil
+	return toDelete, nil
+}
+
+func (s *DataStorage) iterFiles(ctx context.Context, iter func(ctx context.Context, path string, entry fs.FileInfo) error) error {
+	return filepath.Walk(s.baseDir,
+		func(path string, info fs.FileInfo, err error) error {
+			if err != nil {
+				return err
+			}
+			if ctx.Err() != nil {
+				return ctx.Err()
+			}
+
+			if info.IsDir() {
+				return nil
+			}
+
+			return iter(ctx, path, info)
+		})
+}
+
+func (s *DataStorage) Dedupe(ctx context.Context) (uint64, error) {
+	ctx, span := tracer.Start(ctx, fmt.Sprintf("Dedupe"))
+	defer span.End()
+
+	log := rlog.FunctionLog(s.log, "Dedupe")
+
+	sizeMap := map[int64][]string{}
+	err := s.iterFiles(ctx, func(ctx context.Context, path string, info fs.FileInfo) error {
+		size := info.Size()
+		sizeMap[size] = append(sizeMap[size], path)
+		return nil
+	})
+	if err != nil {
+		return 0, err
+	}
+
+	maps.DeleteFunc(sizeMap, func(k int64, v []string) bool {
+		return len(v) <= 1
+	})
+
+	span.AddEvent("collected files with same size", trace.WithAttributes(
+		attribute.Int("count", len(sizeMap)),
+	))
+
+	var deduped uint64 = 0
+
+	i := 0
+	for _, paths := range sizeMap {
+		if i%100 == 0 {
+			log.Info("deduping in progress", "current", i, "total", len(sizeMap))
+		}
+		i++
+
+		if ctx.Err() != nil {
+			return deduped, ctx.Err()
+		}
+
+		slices.Sort(paths)
+		paths = slices.Compact(paths)
+		if len(paths) <= 1 {
+			continue
+		}
+
+		paths, err = applyErr(paths, filepath.Abs)
+		if err != nil {
+			return deduped, err
+		}
+
+		dedupedGroup, err := s.dedupeFiles(ctx, paths)
+		if err != nil {
+			log.Error("Error applying dedupe", "files", paths, "error", err.Error())
+			continue
+		}
+
+		if dedupedGroup > 0 {
+			deduped += dedupedGroup
+			log.Info("deduped file group",
+				slog.String("files", fmt.Sprint(paths)),
+				slog.String("deduped", humanize.Bytes(dedupedGroup)),
+				slog.String("deduped_total", humanize.Bytes(deduped)),
+			)
+		}
+
+	}
+
+	return deduped, nil
+}
+
+func applyErr[E, O any](in []E, apply func(E) (O, error)) ([]O, error) {
+	out := make([]O, 0, len(in))
+	for _, p := range in {
+		o, err := apply(p)
+		if err != nil {
+			return out, err
+		}
+		out = append(out, o)
+
+	}
+	return out, nil
+}
+
+// const blockSize uint64 = 4096
+
+func (s *DataStorage) dedupeFiles(ctx context.Context, paths []string) (deduped uint64, err error) {
+	ctx, span := tracer.Start(ctx, fmt.Sprintf("dedupeFiles"), trace.WithAttributes(
+		attribute.StringSlice("files", paths),
+	))
+	defer func() {
+		span.SetAttributes(attribute.Int64("deduped", int64(deduped)))
+		if err != nil {
+			span.RecordError(err)
+		}
+		span.End()
+	}()
+
+	log := rlog.FunctionLog(s.log, "dedupeFiles")
+
+	srcF, err := os.Open(paths[0])
+	if err != nil {
+		return deduped, err
+	}
+	defer srcF.Close()
+	srcStat, err := srcF.Stat()
+	if err != nil {
+		return deduped, err
+	}
+
+	srcFd := int(srcF.Fd())
+	srcSize := srcStat.Size()
+
+	fsStat := unix.Statfs_t{}
+	err = unix.Fstatfs(srcFd, &fsStat)
+	if err != nil {
+		span.RecordError(err)
+		return deduped, err
+	}
+
+	srcHash, err := filehash(srcF)
+	if err != nil {
+		return deduped, err
+	}
+
+	if fsStat.Bsize > srcSize { // for btrfs it means file in residing in not deduplicatable metadata
+		return deduped, nil
+	}
+
+	blockSize := uint64((srcSize % fsStat.Bsize) * fsStat.Bsize)
+
+	span.SetAttributes(attribute.Int64("blocksize", int64(blockSize)))
+
+	rng := unix.FileDedupeRange{
+		Src_offset: 0,
+		Src_length: blockSize,
+		Info:       []unix.FileDedupeRangeInfo{},
+	}
+
+	for _, dst := range paths[1:] {
+		if ctx.Err() != nil {
+			return deduped, ctx.Err()
+		}
+
+		destF, err := os.OpenFile(dst, os.O_RDWR, os.ModePerm)
+		if err != nil {
+			return deduped, err
+		}
+		defer destF.Close()
+
+		dstHash, err := filehash(destF)
+		if err != nil {
+			return deduped, err
+		}
+
+		if srcHash != dstHash {
+			destF.Close()
+			continue
+		}
+
+		rng.Info = append(rng.Info, unix.FileDedupeRangeInfo{
+			Dest_fd:     int64(destF.Fd()),
+			Dest_offset: 0,
+		})
+	}
+
+	if len(rng.Info) == 0 {
+		return deduped, nil
+	}
+
+	log.Info("found same files, deduping", "files", paths, "size", humanize.Bytes(uint64(srcStat.Size())))
+
+	if ctx.Err() != nil {
+		return deduped, ctx.Err()
+	}
+
+	rng.Src_offset = 0
+	for i := range rng.Info {
+		rng.Info[i].Dest_offset = 0
+	}
+
+	err = unix.IoctlFileDedupeRange(srcFd, &rng)
+	if err != nil {
+		return deduped, err
+	}
+
+	for i := range rng.Info {
+		deduped += rng.Info[i].Bytes_deduped
+
+		rng.Info[i].Status = 0
+		rng.Info[i].Bytes_deduped = 0
+	}
+
+	return deduped, nil
+}
+
+const compareBlockSize = 1024 * 128
+
+func filehash(r io.Reader) ([20]byte, error) {
+	buf := make([]byte, compareBlockSize)
+	_, err := r.Read(buf)
+	if err != nil && err != io.EOF {
+		return [20]byte{}, err
+	}
+
+	return sha1.Sum(buf), nil
 }
 
 func ptr[D any](v D) *D {
diff --git a/src/host/service/service.go b/src/host/service/service.go
index c746119..a5809c7 100644
--- a/src/host/service/service.go
+++ b/src/host/service/service.go
@@ -1,21 +1,27 @@
 package service
 
 import (
+	"bufio"
 	"context"
 	"fmt"
 	"log/slog"
 	"os"
-	"path"
 	"path/filepath"
 	"slices"
 	"strings"
+	"sync"
 
 	"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"
+	"go.opentelemetry.io/otel"
+	"go.opentelemetry.io/otel/attribute"
+	"go.opentelemetry.io/otel/trace"
 	"go.uber.org/multierr"
 	"golang.org/x/exp/maps"
 
@@ -27,6 +33,8 @@ import (
 	"github.com/royalcat/kv"
 )
 
+var tracer = otel.Tracer("git.kmsign.ru/royalcat/tstor/src/service")
+
 type DirAquire struct {
 	Name   string
 	Hashes []infohash.T
@@ -39,9 +47,11 @@ type Service struct {
 
 	torrentLoaded chan struct{}
 
+	loadMutex sync.Mutex
+
 	// stats *Stats
 	DefaultPriority types.PiecePriority
-	Storage         datastorage.DataStorage
+	Storage         *datastorage.DataStorage
 	SourceDir       string
 
 	dirsAquire kv.Store[string, DirAquire]
@@ -50,9 +60,9 @@ type Service struct {
 }
 
 func NewService(sourceDir string, cfg config.TorrentClient, c *torrent.Client,
-	storage datastorage.DataStorage, excludedFiles *store.FilesMappings, infoBytes *store.InfoBytes,
+	storage *datastorage.DataStorage, excludedFiles *store.FilesMappings, infoBytes *store.InfoBytes,
 ) (*Service, error) {
-	dirsAcquire, err := kv.NewBadgerKV[string, DirAquire](path.Join(cfg.MetadataFolder, "dir-acquire"))
+	dirsAcquire, err := tkv.New[string, DirAquire](cfg.MetadataFolder, "dir-acquire")
 	if err != nil {
 		return nil, err
 	}
@@ -66,12 +76,15 @@ func NewService(sourceDir string, cfg config.TorrentClient, c *torrent.Client,
 		Storage:         storage,
 		SourceDir:       sourceDir,
 		torrentLoaded:   make(chan struct{}),
+		loadMutex:       sync.Mutex{},
 		dirsAquire:      dirsAcquire,
+
 		// stats:       newStats(), // TODO persistent
 	}
 
 	go func() {
-		err := s.loadTorrentFiles(context.Background())
+		ctx := context.Background()
+		err := s.loadTorrentFiles(ctx)
 		if err != nil {
 			s.log.Error("initial torrent load failed", "error", err)
 		}
@@ -89,20 +102,32 @@ func (s *Service) Close() error {
 	return err
 }
 
-func (s *Service) AddTorrent(ctx context.Context, f vfs.File) (*torrent.Torrent, error) {
+func (s *Service) LoadTorrent(ctx context.Context, f vfs.File) (*torrent.Torrent, error) {
+	ctx, span := tracer.Start(ctx, "LoadTorrent")
+	defer span.End()
+
+	log := rlog.FunctionLog(s.log, "LoadTorrent")
+
 	defer f.Close(ctx)
 
-	stat, err := f.Stat()
+	stat, err := f.Info()
 	if err != nil {
 		return nil, fmt.Errorf("call stat failed: %w", err)
 	}
 
-	mi, err := metainfo.Load(ctxio.IoReader(ctx, f))
+	span.SetAttributes(attribute.String("filename", stat.Name()))
+
+	mi, err := metainfo.Load(bufio.NewReader(ctxio.IoReader(ctx, f)))
 	if err != nil {
 		return nil, fmt.Errorf("loading torrent metadata from file %s, error: %w", stat.Name(), err)
 	}
+
 	t, ok := s.c.Torrent(mi.HashInfoBytes())
 	if !ok {
+
+		span.AddEvent("torrent not found, loading from file")
+		log.InfoContext(ctx, "torrent not found, loading from file")
+
 		spec, err := torrent.TorrentSpecFromMetaInfoErr(mi)
 		if err != nil {
 			return nil, fmt.Errorf("parse spec from metadata: %w", err)
@@ -110,33 +135,18 @@ func (s *Service) AddTorrent(ctx context.Context, f vfs.File) (*torrent.Torrent,
 		infoBytes := spec.InfoBytes
 
 		if !isValidInfoHashBytes(infoBytes) {
+			log.WarnContext(ctx, "info loaded from spec not valid")
 			infoBytes = nil
 		}
 
 		if len(infoBytes) == 0 {
+			log.InfoContext(ctx, "no info loaded from file, try to load from cache")
 			infoBytes, err = s.infoBytes.GetBytes(spec.InfoHash)
 			if err != nil && err != store.ErrNotFound {
 				return nil, fmt.Errorf("get info bytes from database: %w", err)
 			}
 		}
 
-		var info metainfo.Info
-		err = bencode.Unmarshal(infoBytes, &info)
-		if err != nil {
-			infoBytes = nil
-		} else {
-			compatable, _, err := s.checkTorrentCompatable(ctx, spec.InfoHash, info)
-			if err != nil {
-				return nil, err
-			}
-			if !compatable {
-				return nil, fmt.Errorf(
-					"torrent with name '%s' not compatable existing infohash: %s, new: %s",
-					t.Name(), t.InfoHash().HexString(), spec.InfoHash.HexString(),
-				)
-			}
-		}
-
 		t, _ = s.c.AddTorrentOpt(torrent.AddTorrentOpts{
 			InfoHash:  spec.InfoHash,
 			Storage:   s.Storage,
@@ -146,18 +156,33 @@ func (s *Service) AddTorrent(ctx context.Context, f vfs.File) (*torrent.Torrent,
 		t.AllowDataDownload()
 		t.AllowDataUpload()
 
+		span.AddEvent("torrent added to client")
+
 		select {
 		case <-ctx.Done():
-			return nil, fmt.Errorf("creating torrent timed out")
+			return nil, ctx.Err()
 		case <-t.GotInfo():
 			err := s.infoBytes.Set(t.InfoHash(), t.Metainfo())
 			if err != nil {
 				s.log.Error("error setting info bytes for torrent %s: %s", t.Name(), err.Error())
 			}
-			for _, f := range t.Files() {
-				f.SetPriority(s.DefaultPriority)
-			}
+		}
+		span.AddEvent("got info")
 
+		info := t.Info()
+		if info == nil {
+			return nil, fmt.Errorf("info is nil")
+		}
+
+		compatable, _, err := s.checkTorrentCompatable(ctx, spec.InfoHash, *info)
+		if err != nil {
+			return nil, err
+		}
+		if !compatable {
+			return nil, fmt.Errorf(
+				"torrent with name '%s' not compatable existing infohash: %s, new: %s",
+				t.Name(), t.InfoHash().HexString(), spec.InfoHash.HexString(),
+			)
 		}
 	}
 
@@ -271,15 +296,15 @@ func (s *Service) checkTorrentFilesCompatable(aq DirAquire, existingFiles, newFi
 	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 (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
@@ -290,12 +315,12 @@ func isValidInfoHashBytes(d []byte) bool {
 func (s *Service) NewTorrentFs(ctx context.Context, f vfs.File) (vfs.Filesystem, error) {
 	defer f.Close(ctx)
 
-	info, err := f.Stat()
+	info, err := f.Info()
 	if err != nil {
 		return nil, err
 	}
 
-	t, err := s.AddTorrent(ctx, f)
+	t, err := s.LoadTorrent(ctx, f)
 	if err != nil {
 		return nil, err
 	}
@@ -311,7 +336,46 @@ func (s *Service) GetStats() torrent.ConnStats {
 	return s.c.ConnStats()
 }
 
+const loadWorkers = 5
+
 func (s *Service) loadTorrentFiles(ctx context.Context) error {
+	ctx, span := tracer.Start(ctx, "loadTorrentFiles", trace.WithAttributes(
+		attribute.Int("workers", loadWorkers),
+	))
+	defer span.End()
+
+	log := rlog.FunctionLog(s.log, "loadTorrentFiles")
+
+	loaderPaths := make(chan string)
+	wg := sync.WaitGroup{}
+
+	defer func() {
+		close(loaderPaths)
+		wg.Wait()
+	}()
+
+	loaderWorker := func() {
+		wg.Add(1)
+		for path := range loaderPaths {
+			file, err := vfs.NewLazyOsFile(path)
+			if err != nil {
+				log.Error("error opening torrent file", "filename", path, rlog.Err(err))
+				continue
+			}
+			defer file.Close(ctx)
+
+			_, err = s.LoadTorrent(ctx, file)
+			if err != nil {
+				s.log.Error("failed adding torrent", "error", err)
+			}
+		}
+		wg.Done()
+	}
+
+	for range loadWorkers {
+		go loaderWorker()
+	}
+
 	return filepath.Walk(s.SourceDir, func(path string, info os.FileInfo, err error) error {
 		if err != nil {
 			return fmt.Errorf("fs walk error: %w", err)
@@ -326,13 +390,7 @@ func (s *Service) loadTorrentFiles(ctx context.Context) error {
 		}
 
 		if strings.HasSuffix(path, ".torrent") {
-			file := vfs.NewLazyOsFile(path)
-			defer file.Close(ctx)
-
-			_, err = s.AddTorrent(ctx, file)
-			if err != nil {
-				s.log.Error("failed adding torrent", "error", err)
-			}
+			loaderPaths <- path
 		}
 
 		return nil
diff --git a/src/host/tkv/new.go b/src/host/tkv/new.go
new file mode 100644
index 0000000..3f740ea
--- /dev/null
+++ b/src/host/tkv/new.go
@@ -0,0 +1,21 @@
+package tkv
+
+import (
+	"path"
+
+	"git.kmsign.ru/royalcat/tstor/pkg/kvtrace"
+	"github.com/royalcat/kv"
+	"go.opentelemetry.io/otel/attribute"
+)
+
+func New[K kv.Bytes, V any](dbdir, name string) (store kv.Store[K, V], err error) {
+	dir := path.Join(dbdir, name)
+	store, err = kv.NewBadgerKV[K, V](dir)
+	if err != nil {
+		return nil, err
+	}
+
+	store = kvtrace.WrapTracing(store, attribute.String("collection", name), attribute.String("database", "badger"))
+
+	return store, err
+}
diff --git a/src/host/vfs/archive.go b/src/host/vfs/archive.go
index cf1e1a6..4717ec8 100644
--- a/src/host/vfs/archive.go
+++ b/src/host/vfs/archive.go
@@ -3,40 +3,40 @@ package vfs
 import (
 	"archive/zip"
 	"context"
+	"fmt"
 	"io"
 	"io/fs"
-	"os"
 	"path"
-	"path/filepath"
 	"strings"
+	"sync"
+	"time"
 
 	"git.kmsign.ru/royalcat/tstor/pkg/ctxio"
-	"git.kmsign.ru/royalcat/tstor/src/iio"
 	"github.com/bodgit/sevenzip"
 	"github.com/nwaples/rardecode/v2"
 )
 
 var ArchiveFactories = map[string]FsFactory{
 	".zip": func(ctx context.Context, f File) (Filesystem, error) {
-		stat, err := f.Stat()
+		stat, err := f.Info()
 		if err != nil {
 			return nil, err
 		}
-		return NewArchive(ctx, stat.Name(), f, stat.Size(), ZipLoader), nil
+		return NewArchive(ctx, stat.Name(), f, stat.Size(), ZipLoader)
 	},
 	".rar": func(ctx context.Context, f File) (Filesystem, error) {
-		stat, err := f.Stat()
+		stat, err := f.Info()
 		if err != nil {
 			return nil, err
 		}
-		return NewArchive(ctx, stat.Name(), f, stat.Size(), RarLoader), nil
+		return NewArchive(ctx, stat.Name(), f, stat.Size(), RarLoader)
 	},
 	".7z": func(ctx context.Context, f File) (Filesystem, error) {
-		stat, err := f.Stat()
+		stat, err := f.Info()
 		if err != nil {
 			return nil, err
 		}
-		return NewArchive(ctx, stat.Name(), f, stat.Size(), SevenZipLoader), nil
+		return NewArchive(ctx, stat.Name(), f, stat.Size(), SevenZipLoader)
 	},
 }
 
@@ -47,52 +47,73 @@ var _ Filesystem = &ArchiveFS{}
 type ArchiveFS struct {
 	name string
 
-	r ctxio.ReaderAt
+	size int64
 
-	Size int64
-
-	files func() (map[string]File, error)
+	files map[string]File
 }
 
-func NewArchive(ctx context.Context, name string, r ctxio.ReaderAt, size int64, loader archiveLoader) *ArchiveFS {
-	return &ArchiveFS{
-		name: name,
-		r:    r,
-		Size: size,
-		files: OnceValueWOErr(func() (map[string]File, error) {
-			zipFiles, err := loader(ctx, r, size)
-			if err != nil {
-				return nil, err
-			}
-			// TODO make optional
-			singleDir := true
-			for k := range zipFiles {
-				if !strings.HasPrefix(k, "/"+name+"/") {
-					singleDir = false
-					break
-				}
-			}
+// ModTime implements Filesystem.
+func (a *ArchiveFS) ModTime() time.Time {
+	return time.Time{}
+}
 
-			files := make(map[string]File, len(zipFiles))
-			for k, v := range zipFiles {
-				// TODO make optional
-				if strings.Contains(k, "/__MACOSX/") {
-					continue
-				}
+// Mode implements Filesystem.
+func (a *ArchiveFS) Mode() fs.FileMode {
+	return fs.ModeDir
+}
 
-				if singleDir {
-					k, _ = strings.CutPrefix(k, "/"+name)
-				}
+// Size implements Filesystem.
+func (a *ArchiveFS) Size() int64 {
+	return int64(a.size)
+}
 
-				files[k] = v
-			}
+// Sys implements Filesystem.
+func (a *ArchiveFS) Sys() any {
+	return nil
+}
 
-			// FIXME
-			files["/.forcegallery"] = NewMemoryFile(".forcegallery", []byte{})
+// FsName implements Filesystem.
+func (a *ArchiveFS) FsName() string {
+	return "archivefs"
+}
 
-			return files, nil
-		}),
+func NewArchive(ctx context.Context, name string, r ctxio.ReaderAt, size int64, loader archiveLoader) (*ArchiveFS, error) {
+	archiveFiles, err := loader(ctx, r, size)
+	if err != nil {
+		return nil, err
 	}
+
+	// TODO make optional
+	singleDir := true
+	for k := range archiveFiles {
+		if !strings.HasPrefix(k, "/"+name+"/") {
+			singleDir = false
+			break
+		}
+	}
+
+	files := make(map[string]File, len(archiveFiles))
+	for k, v := range archiveFiles {
+		// TODO make optional
+		if strings.Contains(k, "/__MACOSX/") {
+			continue
+		}
+
+		if singleDir {
+			k, _ = strings.CutPrefix(k, "/"+name)
+		}
+
+		files[k] = v
+	}
+
+	// FIXME
+	files["/.forcegallery"] = NewMemoryFile(".forcegallery", []byte{})
+
+	return &ArchiveFS{
+		name:  name,
+		size:  size,
+		files: files,
+	}, nil
 }
 
 // Unlink implements Filesystem.
@@ -101,35 +122,21 @@ func (a *ArchiveFS) Unlink(ctx context.Context, filename string) error {
 }
 
 func (a *ArchiveFS) Open(ctx context.Context, filename string) (File, error) {
-	files, err := a.files()
-	if err != nil {
-		return nil, err
-	}
-
-	return getFile(files, filename)
+	return getFile(a.files, filename)
 }
 
-func (fs *ArchiveFS) ReadDir(ctx context.Context, path string) ([]fs.DirEntry, error) {
-	files, err := fs.files()
-	if err != nil {
-		return nil, err
-	}
-
-	return listDirFromFiles(files, path)
+func (a *ArchiveFS) ReadDir(ctx context.Context, path string) ([]fs.DirEntry, error) {
+	return listDirFromFiles(a.files, path)
 }
 
 // Stat implements Filesystem.
 func (afs *ArchiveFS) Stat(ctx context.Context, filename string) (fs.FileInfo, error) {
-	files, err := afs.files()
-	if err != nil {
-		return nil, err
+
+	if file, ok := afs.files[filename]; ok {
+		return file.Info()
 	}
 
-	if file, ok := files[filename]; ok {
-		return file.Stat()
-	}
-
-	for p, _ := range files {
+	for p, _ := range afs.files {
 		if strings.HasPrefix(p, filename) {
 			return newDirInfo(path.Base(filename)), nil
 		}
@@ -140,11 +147,7 @@ func (afs *ArchiveFS) Stat(ctx context.Context, filename string) (fs.FileInfo, e
 
 // Info implements Filesystem.
 func (a *ArchiveFS) Info() (fs.FileInfo, error) {
-	return &fileInfo{
-		name:  a.name,
-		size:  a.Size,
-		isDir: true,
-	}, nil
+	return a, nil
 }
 
 // IsDir implements Filesystem.
@@ -162,42 +165,46 @@ func (a *ArchiveFS) Type() fs.FileMode {
 	return fs.ModeDir
 }
 
-var _ File = &archiveFile{}
+var _ File = (*archiveFile)(nil)
 
-func NewArchiveFile(name string, readerFunc func() (iio.Reader, error), size int64) *archiveFile {
+func NewArchiveFile(name string, size int64, af archiveFileReaderFactory) *archiveFile {
 	return &archiveFile{
-		name:       name,
-		readerFunc: readerFunc,
-		size:       size,
+		name: name,
+		size: size,
+		af:   af,
+
+		buffer: ctxio.NewFileBuffer(nil),
 	}
 }
 
+const readahead = 1024 * 16
+
 type archiveFile struct {
 	name string
+	size int64
+	af   archiveFileReaderFactory
 
-	readerFunc func() (iio.Reader, error)
-	reader     iio.Reader
-	size       int64
+	m sync.Mutex
+
+	offset int64
+	readen int64
+	buffer *ctxio.FileBuffer
 }
 
-func (d *archiveFile) Stat() (fs.FileInfo, error) {
+// Name implements File.
+func (d *archiveFile) Name() string {
+	return d.name
+}
+
+// Type implements File.
+func (d *archiveFile) Type() fs.FileMode {
+	return roMode
+}
+
+func (d *archiveFile) Info() (fs.FileInfo, error) {
 	return newFileInfo(d.name, d.size), nil
 }
 
-func (d *archiveFile) load() error {
-	if d.reader != nil {
-		return nil
-	}
-	r, err := d.readerFunc()
-	if err != nil {
-		return err
-	}
-
-	d.reader = r
-
-	return nil
-}
-
 func (d *archiveFile) Size() int64 {
 	return d.size
 }
@@ -206,31 +213,60 @@ func (d *archiveFile) IsDir() bool {
 	return false
 }
 
-func (d *archiveFile) Close(ctx context.Context) (err error) {
-	if d.reader != nil {
-		err = d.reader.Close()
-		d.reader = nil
+func (d *archiveFile) Close(ctx context.Context) error {
+	return d.buffer.Close(ctx)
+}
+
+func (d *archiveFile) loadMore(ctx context.Context, to int64) error {
+	d.m.Lock()
+	defer d.m.Unlock()
+
+	if to < d.readen {
+		return nil
 	}
 
-	return
+	reader, err := d.af(ctx)
+	if err != nil {
+		return fmt.Errorf("failed to get file reader: %w", err)
+	}
+	_, err = d.buffer.Seek(0, io.SeekStart)
+	if err != nil {
+		return fmt.Errorf("failed to seek to start of the file: %w", err)
+	}
+	d.readen, err = ctxio.CopyN(ctx, d.buffer, ctxio.WrapIoReader(reader), to+readahead)
+	if err != nil && err != io.EOF {
+		return fmt.Errorf("error copying from archive file reader: %w", err)
+	}
+
+	return nil
 }
 
 func (d *archiveFile) Read(ctx context.Context, p []byte) (n int, err error) {
-	if err := d.load(); err != nil {
-		return 0, err
+	err = d.loadMore(ctx, d.offset+int64(len(p)))
+	if err != nil {
+		return 0, fmt.Errorf("failed to load more from archive file: %w", err)
 	}
-
-	return d.reader.Read(p)
+	n, err = d.buffer.Read(ctx, p)
+	if err != nil && err != io.EOF {
+		return n, fmt.Errorf("failed to read from buffer: %w", err)
+	}
+	return n, nil
 }
 
 func (d *archiveFile) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) {
-	if err := d.load(); err != nil {
-		return 0, err
+	err = d.loadMore(ctx, off+int64(len(p)))
+	if err != nil {
+		return 0, fmt.Errorf("failed to load more from archive file: %w", err)
 	}
-
-	return d.reader.ReadAt(p, off)
+	n, err = d.buffer.ReadAt(ctx, p, off)
+	if err != nil && err != io.EOF {
+		return n, fmt.Errorf("failed to read from buffer: %w", err)
+	}
+	return n, nil
 }
 
+type archiveFileReaderFactory func(ctx context.Context) (io.ReadCloser, error)
+
 var _ archiveLoader = ZipLoader
 
 func ZipLoader(ctx context.Context, ctxreader ctxio.ReaderAt, size int64) (map[string]*archiveFile, error) {
@@ -248,16 +284,24 @@ func ZipLoader(ctx context.Context, ctxreader ctxio.ReaderAt, size int64) (map[s
 			continue
 		}
 
-		rf := func() (iio.Reader, error) {
-			zr, err := zipFile.Open()
+		i := i
+		af := func(ctx context.Context) (io.ReadCloser, error) {
+			reader := ctxio.IoReaderAt(ctx, ctxreader)
+
+			zr, err := zip.NewReader(reader, size)
 			if err != nil {
 				return nil, err
 			}
 
-			return iio.NewDiskTeeReader(zr)
+			rc, err := zr.File[i].Open()
+			if err != nil {
+				return nil, err
+			}
+
+			return rc, nil
 		}
 
-		out[AbsPath(zipFile.Name)] = NewArchiveFile(zipFile.Name, rf, zipFile.FileInfo().Size())
+		out[AbsPath(zipFile.Name)] = NewArchiveFile(zipFile.Name, zipFile.FileInfo().Size(), af)
 	}
 
 	return out, nil
@@ -274,25 +318,29 @@ func SevenZipLoader(ctx context.Context, ctxreader ctxio.ReaderAt, size int64) (
 	}
 
 	out := make(map[string]*archiveFile)
-	for _, f := range r.File {
+	for i, f := range r.File {
 		f := f
 		if f.FileInfo().IsDir() {
 			continue
 		}
 
-		rf := func() (iio.Reader, error) {
-			zr, err := f.Open()
+		i := i
+		af := func(ctx context.Context) (io.ReadCloser, error) {
+			reader := ctxio.IoReaderAt(ctx, ctxreader)
+			zr, err := sevenzip.NewReader(reader, size)
 			if err != nil {
 				return nil, err
 			}
 
-			return iio.NewDiskTeeReader(zr)
+			rc, err := zr.File[i].Open()
+			if err != nil {
+				return nil, err
+			}
+
+			return rc, nil
 		}
 
-		af := NewArchiveFile(f.Name, rf, f.FileInfo().Size())
-		n := filepath.Join(string(os.PathSeparator), f.Name)
-
-		out[n] = af
+		out[AbsPath(f.Name)] = NewArchiveFile(f.Name, f.FileInfo().Size(), af)
 	}
 
 	return out, nil
@@ -318,15 +366,26 @@ func RarLoader(ctx context.Context, ctxreader ctxio.ReaderAt, size int64) (map[s
 			return nil, err
 		}
 
-		rf := func() (iio.Reader, error) {
-			return iio.NewDiskTeeReader(r)
+		name := header.Name
+		af := func(ctx context.Context) (io.ReadCloser, error) {
+			reader := ctxio.IoReadSeekerWrapper(ctx, ctxreader, size)
+			r, err := rardecode.NewReader(reader)
+			if err != nil {
+				return nil, err
+			}
+
+			for header, err := r.Next(); err != io.EOF; header, err = r.Next() {
+				if err != nil {
+					return nil, err
+				}
+				if header.Name == name {
+					return io.NopCloser(r), nil
+				}
+			}
+			return nil, fmt.Errorf("file with name '%s' not found", name)
 		}
 
-		n := filepath.Join(string(os.PathSeparator), header.Name)
-
-		af := NewArchiveFile(header.Name, rf, header.UnPackedSize)
-
-		out[n] = af
+		out[AbsPath(header.Name)] = NewArchiveFile(header.Name, header.UnPackedSize, af)
 	}
 
 	return out, nil
diff --git a/src/host/vfs/archive_test.go b/src/host/vfs/archive_test.go
index c6ae444..443abe2 100644
--- a/src/host/vfs/archive_test.go
+++ b/src/host/vfs/archive_test.go
@@ -1,4 +1,4 @@
-package vfs
+package vfs_test
 
 import (
 	"archive/zip"
@@ -8,9 +8,35 @@ import (
 	"testing"
 
 	"git.kmsign.ru/royalcat/tstor/pkg/ctxio"
+	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
 	"github.com/stretchr/testify/require"
 )
 
+// TODO
+// func TestArchiveFactories(t *testing.T) {
+// 	t.Parallel()
+
+// 	ctx := context.Background()
+
+// 	require := require.New(t)
+
+// 	require.Contains(vfs.ArchiveFactories, ".zip")
+// 	require.Contains(vfs.ArchiveFactories, ".rar")
+// 	require.Contains(vfs.ArchiveFactories, ".7z")
+
+// 	fs, err := vfs.ArchiveFactories[".zip"](ctx, &vfs.DummyFile{})
+// 	require.NoError(err)
+// 	require.NotNil(fs)
+
+// 	fs, err = vfs.ArchiveFactories[".rar"](ctx, &vfs.DummyFile{})
+// 	require.NoError(err)
+// 	require.NotNil(fs)
+
+// 	fs, err = vfs.ArchiveFactories[".7z"](ctx, &vfs.DummyFile{})
+// 	require.NoError(err)
+// 	require.NotNil(fs)
+// }
+
 var fileContent []byte = []byte("Hello World")
 
 func TestZipFilesystem(t *testing.T) {
@@ -22,7 +48,8 @@ func TestZipFilesystem(t *testing.T) {
 	ctx := context.Background()
 
 	// TODO add single dir collapse test
-	zfs := NewArchive(ctx, "test", zReader, size, ZipLoader)
+	zfs, err := vfs.NewArchive(ctx, "test", zReader, size, vfs.ZipLoader)
+	require.NoError(err)
 
 	files, err := zfs.ReadDir(ctx, "/path/to/test/file")
 	require.NoError(err)
diff --git a/src/host/vfs/dir.go b/src/host/vfs/dir.go
index 6d77249..51b78cc 100644
--- a/src/host/vfs/dir.go
+++ b/src/host/vfs/dir.go
@@ -6,39 +6,54 @@ import (
 	"path"
 )
 
-var _ File = &dir{}
+var _ File = &dirFile{}
 
-func NewDir(name string) File {
-	return &dir{
+func newDirFile(name string) File {
+	return &dirFile{
 		name: path.Base(name),
 	}
 }
 
-type dir struct {
+type dirFile struct {
 	name string
 }
 
-// Info implements File.
-func (d *dir) Stat() (fs.FileInfo, error) {
-	return newDirInfo(d.name), nil
-}
-
-func (d *dir) Size() int64 {
-	return 0
-}
-
-func (d *dir) IsDir() bool {
-	return true
-}
-
-func (d *dir) Close(ctx context.Context) error {
+// Close implements File.
+func (d *dirFile) Close(ctx context.Context) error {
 	return nil
 }
 
-func (d *dir) Read(ctx context.Context, p []byte) (n int, err error) {
-	return 0, nil
+// Info implements File.
+func (d *dirFile) Info() (fs.FileInfo, error) {
+	return newDirInfo(d.name), nil
 }
 
-func (d *dir) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) {
-	return 0, nil
+// IsDir implements File.
+func (d *dirFile) IsDir() bool {
+	return true
+}
+
+// Name implements File.
+func (d *dirFile) Name() string {
+	return d.name
+}
+
+// Read implements File.
+func (d *dirFile) Read(ctx context.Context, p []byte) (n int, err error) {
+	return 0, fs.ErrInvalid
+}
+
+// ReadAt implements File.
+func (d *dirFile) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) {
+	return 0, fs.ErrInvalid
+}
+
+// Size implements File.
+func (d *dirFile) Size() int64 {
+	return 0
+}
+
+// Type implements File.
+func (d *dirFile) Type() fs.FileMode {
+	return roMode | fs.ModeDir
 }
diff --git a/src/host/vfs/dummy.go b/src/host/vfs/dummy.go
new file mode 100644
index 0000000..0f57da7
--- /dev/null
+++ b/src/host/vfs/dummy.go
@@ -0,0 +1,125 @@
+package vfs
+
+import (
+	"context"
+	"io/fs"
+	"os"
+	"path"
+	"time"
+)
+
+var _ Filesystem = &DummyFs{}
+
+type DummyFs struct {
+	name string
+}
+
+// ModTime implements Filesystem.
+func (d *DummyFs) ModTime() time.Time {
+	return time.Time{}
+}
+
+// Mode implements Filesystem.
+func (d *DummyFs) Mode() fs.FileMode {
+	return fs.ModeDir
+}
+
+// Size implements Filesystem.
+func (d *DummyFs) Size() int64 {
+	panic("unimplemented")
+}
+
+// Sys implements Filesystem.
+func (d *DummyFs) Sys() any {
+	panic("unimplemented")
+}
+
+// FsName implements Filesystem.
+func (d *DummyFs) FsName() string {
+	return "dummyfs"
+}
+
+// Stat implements Filesystem.
+func (*DummyFs) Stat(ctx context.Context, filename string) (fs.FileInfo, error) {
+	return newFileInfo(path.Base(filename), 0), nil // TODO
+}
+
+func (d *DummyFs) Open(ctx context.Context, filename string) (File, error) {
+	return &DummyFile{}, nil
+}
+
+func (d *DummyFs) Unlink(ctx context.Context, filename string) error {
+	return ErrNotImplemented
+}
+
+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),
+		}, nil
+	}
+
+	return nil, os.ErrNotExist
+}
+
+// Info implements Filesystem.
+func (d *DummyFs) Info() (fs.FileInfo, error) {
+	return newDirInfo(d.name), nil
+}
+
+// IsDir implements Filesystem.
+func (d *DummyFs) IsDir() bool {
+	return true
+}
+
+// Name implements Filesystem.
+func (d *DummyFs) Name() string {
+	return d.name
+}
+
+// Type implements Filesystem.
+func (d *DummyFs) Type() fs.FileMode {
+	return fs.ModeDir
+}
+
+var _ File = &DummyFile{}
+
+type DummyFile struct {
+	name string
+}
+
+// Name implements File.
+func (d *DummyFile) Name() string {
+	panic("unimplemented")
+}
+
+// Type implements File.
+func (d *DummyFile) Type() fs.FileMode {
+	panic("unimplemented")
+}
+
+// Stat implements File.
+func (d *DummyFile) Info() (fs.FileInfo, error) {
+	return newFileInfo(d.name, 0), nil
+}
+
+func (d *DummyFile) Size() int64 {
+	return 0
+}
+
+func (d *DummyFile) IsDir() bool {
+	return false
+}
+
+func (d *DummyFile) Close(ctx context.Context) error {
+	return nil
+}
+
+func (d *DummyFile) Read(ctx context.Context, p []byte) (n int, err error) {
+	return 0, nil
+}
+
+func (d *DummyFile) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) {
+	return 0, nil
+}
diff --git a/src/host/vfs/fs.go b/src/host/vfs/fs.go
index 018967f..64f5547 100644
--- a/src/host/vfs/fs.go
+++ b/src/host/vfs/fs.go
@@ -8,12 +8,14 @@ import (
 	"time"
 
 	"git.kmsign.ru/royalcat/tstor/pkg/ctxio"
+	"go.opentelemetry.io/otel"
 )
 
 type File interface {
 	IsDir() bool
 	Size() int64
-	Stat() (fs.FileInfo, error)
+
+	fs.DirEntry
 
 	ctxio.Reader
 	ctxio.ReaderAt
@@ -22,6 +24,8 @@ type File interface {
 
 var ErrNotImplemented = errors.New("not implemented")
 
+var tracer = otel.Tracer("git.kmsign.ru/royalcat/tstor/src/host/vfs")
+
 type Filesystem interface {
 	// Open opens the named file for reading. If successful, methods on the
 	// returned file can be used for reading; the associated file descriptor has
@@ -35,10 +39,12 @@ type Filesystem interface {
 	Stat(ctx context.Context, filename string) (fs.FileInfo, error)
 	Unlink(ctx context.Context, filename string) error
 
+	// As filesystem mounted to some path, make sense to have the filesystem implement DirEntry
 	fs.DirEntry
 }
 
-const defaultMode = fs.FileMode(0555)
+// readonly
+const roMode = fs.FileMode(0555)
 
 type fileInfo struct {
 	name  string
@@ -87,10 +93,10 @@ func (fi *fileInfo) Size() int64 {
 
 func (fi *fileInfo) Mode() fs.FileMode {
 	if fi.isDir {
-		return defaultMode | fs.ModeDir
+		return roMode | fs.ModeDir
 	}
 
-	return defaultMode
+	return roMode
 }
 
 func (fi *fileInfo) ModTime() time.Time {
diff --git a/src/host/vfs/fs_test.go b/src/host/vfs/fs_test.go
index 3caef23..3ea2dcc 100644
--- a/src/host/vfs/fs_test.go
+++ b/src/host/vfs/fs_test.go
@@ -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(defaultMode|fs.ModeDir, fi.Mode())
+	require.Equal(roMode|fs.ModeDir, fi.Mode())
 	require.Nil(fi.Sys())
 
 }
diff --git a/src/host/vfs/log.go b/src/host/vfs/log.go
index 7560f26..1c19e1f 100644
--- a/src/host/vfs/log.go
+++ b/src/host/vfs/log.go
@@ -5,22 +5,62 @@ import (
 	"io/fs"
 	"log/slog"
 	"reflect"
+	"time"
+
+	"git.kmsign.ru/royalcat/tstor/pkg/rlog"
+	"go.opentelemetry.io/otel/attribute"
+	"go.opentelemetry.io/otel/trace"
 )
 
 type LogFS struct {
 	fs  Filesystem
 	log *slog.Logger
+
+	timeout     time.Duration
+	readTimeout time.Duration
 }
 
 var _ Filesystem = (*LogFS)(nil)
 
-func WrapLogFS(fs Filesystem, log *slog.Logger) *LogFS {
+func WrapLogFS(fs Filesystem) *LogFS {
 	return &LogFS{
-		fs:  fs,
-		log: log.With("component", "fs"),
+		fs:          fs,
+		log:         rlog.ComponentLog("fs"),
+		timeout:     time.Minute * 3,
+		readTimeout: time.Minute,
 	}
 }
 
+// ModTime implements Filesystem.
+func (lfs *LogFS) ModTime() time.Time {
+	return lfs.ModTime()
+}
+
+// Mode implements Filesystem.
+func (lfs *LogFS) Mode() fs.FileMode {
+	return lfs.Mode()
+}
+
+// Size implements Filesystem.
+func (lfs *LogFS) Size() int64 {
+	return lfs.Size()
+}
+
+// Sys implements Filesystem.
+func (lfs *LogFS) Sys() any {
+	return lfs.Sys()
+}
+
+func (fs *LogFS) FsName() string {
+	return "logfs"
+}
+
+func (fs *LogFS) traceAttrs(add ...attribute.KeyValue) trace.SpanStartOption {
+	return trace.WithAttributes(append([]attribute.KeyValue{
+		attribute.String("fs", fs.FsName()),
+	}, add...)...)
+}
+
 // Info implements Filesystem.
 func (fs *LogFS) Info() (fs.FileInfo, error) {
 	return fs.fs.Info()
@@ -42,36 +82,84 @@ func (fs *LogFS) Type() fs.FileMode {
 }
 
 // Open implements Filesystem.
-func (fs *LogFS) Open(ctx context.Context, filename string) (File, error) {
-	file, err := fs.fs.Open(ctx, filename)
+func (fs *LogFS) Open(ctx context.Context, filename string) (file File, err error) {
+	ctx, cancel := context.WithTimeout(ctx, fs.timeout)
+	defer cancel()
+	ctx, span := tracer.Start(ctx, "Open",
+		fs.traceAttrs(attribute.String("filename", filename)),
+	)
+	defer func() {
+		if err != nil {
+			span.RecordError(err)
+		}
+		span.End()
+	}()
+
+	file, err = fs.fs.Open(ctx, filename)
 	if err != nil {
 		fs.log.With("filename", filename).Error("Failed to open file")
 	}
-	file = WrapLogFile(file, filename, fs.log)
+	file = WrapLogFile(file, filename, fs.log, fs.readTimeout)
 	return file, err
 }
 
 // ReadDir implements Filesystem.
-func (fs *LogFS) ReadDir(ctx context.Context, path string) ([]fs.DirEntry, error) {
-	file, err := fs.fs.ReadDir(ctx, path)
+func (fs *LogFS) ReadDir(ctx context.Context, path string) (entries []fs.DirEntry, err error) {
+	ctx, cancel := context.WithTimeout(ctx, fs.timeout)
+	defer cancel()
+	ctx, span := tracer.Start(ctx, "ReadDir",
+		fs.traceAttrs(attribute.String("path", path)),
+	)
+	defer func() {
+		if err != nil {
+			span.RecordError(err)
+		}
+		span.End()
+	}()
+
+	entries, err = fs.fs.ReadDir(ctx, path)
 	if err != nil {
 		fs.log.ErrorContext(ctx, "Failed to read dir", "path", path, "error", err.Error(), "fs-type", reflect.TypeOf(fs.fs).Name())
 	}
-	return file, err
+	return entries, err
 }
 
 // Stat implements Filesystem.
-func (fs *LogFS) Stat(ctx context.Context, filename string) (fs.FileInfo, error) {
-	file, err := fs.fs.Stat(ctx, filename)
+func (fs *LogFS) Stat(ctx context.Context, filename string) (info fs.FileInfo, err error) {
+	ctx, cancel := context.WithTimeout(ctx, fs.timeout)
+	defer cancel()
+	ctx, span := tracer.Start(ctx, "Stat",
+		fs.traceAttrs(attribute.String("filename", filename)),
+	)
+	defer func() {
+		if err != nil {
+			span.RecordError(err)
+		}
+		span.End()
+	}()
+
+	info, err = fs.fs.Stat(ctx, filename)
 	if err != nil {
 		fs.log.Error("Failed to stat", "filename", filename, "error", err)
 	}
-	return file, err
+	return info, err
 }
 
 // Unlink implements Filesystem.
-func (fs *LogFS) Unlink(ctx context.Context, filename string) error {
-	err := fs.fs.Unlink(ctx, filename)
+func (fs *LogFS) Unlink(ctx context.Context, filename string) (err error) {
+	ctx, cancel := context.WithTimeout(ctx, fs.timeout)
+	defer cancel()
+	ctx, span := tracer.Start(ctx, "Unlink",
+		fs.traceAttrs(attribute.String("filename", filename)),
+	)
+	defer func() {
+		if err != nil {
+			span.RecordError(err)
+		}
+		span.End()
+	}()
+
+	err = fs.fs.Unlink(ctx, filename)
 	if err != nil {
 		fs.log.Error("Failed to stat", "filename", filename, "error", err)
 	}
@@ -79,24 +167,51 @@ func (fs *LogFS) Unlink(ctx context.Context, filename string) error {
 }
 
 type LogFile struct {
-	f   File
-	log *slog.Logger
+	filename string
+	f        File
+
+	log     *slog.Logger
+	timeout time.Duration
+}
+
+// Name implements File.
+func (f *LogFile) Name() string {
+	return f.f.Name()
+}
+
+// Type implements File.
+func (f *LogFile) Type() fs.FileMode {
+	return f.f.Type()
 }
 
 var _ File = (*LogFile)(nil)
 
-func WrapLogFile(f File, filename string, log *slog.Logger) *LogFile {
+func WrapLogFile(f File, filename string, log *slog.Logger, timeout time.Duration) *LogFile {
 	return &LogFile{
-		f:   f,
-		log: log.With("filename", filename),
+		filename: filename,
+		f:        f,
+		log:      log.With("filename", filename),
+		timeout:  timeout,
 	}
 }
 
 // Close implements File.
-func (f *LogFile) Close(ctx context.Context) error {
-	err := f.f.Close(ctx)
+func (f *LogFile) Close(ctx context.Context) (err error) {
+	ctx, cancel := context.WithTimeout(ctx, f.timeout)
+	defer cancel()
+	ctx, span := tracer.Start(ctx, "Close",
+		trace.WithAttributes(attribute.String("filename", f.filename)),
+	)
+	defer func() {
+		if err != nil {
+			span.RecordError(err)
+		}
+		span.End()
+	}()
+
+	err = f.f.Close(ctx)
 	if err != nil {
-		f.log.Error("Failed to close", "error", err)
+		f.log.ErrorContext(ctx, "Failed to close", "error", err)
 	}
 	return err
 }
@@ -108,6 +223,22 @@ func (f *LogFile) IsDir() bool {
 
 // Read implements File.
 func (f *LogFile) Read(ctx context.Context, p []byte) (n int, err error) {
+	ctx, cancel := context.WithTimeout(ctx, f.timeout)
+	defer cancel()
+	ctx, span := tracer.Start(ctx, "Read",
+		trace.WithAttributes(
+			attribute.String("filename", f.filename),
+			attribute.Int("length", len(p)),
+		),
+	)
+	defer func() {
+		span.SetAttributes(attribute.Int("read", n))
+		if err != nil {
+			span.RecordError(err)
+		}
+		span.End()
+	}()
+
 	n, err = f.f.Read(ctx, p)
 	if err != nil {
 		f.log.Error("Failed to read", "error", err)
@@ -117,6 +248,22 @@ func (f *LogFile) Read(ctx context.Context, p []byte) (n int, err error) {
 
 // ReadAt implements File.
 func (f *LogFile) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) {
+	ctx, cancel := context.WithTimeout(ctx, f.timeout)
+	defer cancel()
+	ctx, span := tracer.Start(ctx, "ReadAt",
+		trace.WithAttributes(
+			attribute.String("filename", f.filename),
+			attribute.Int("length", len(p)),
+		),
+	)
+	defer func() {
+		span.SetAttributes(attribute.Int("read", n))
+		if err != nil {
+			span.RecordError(err)
+		}
+		span.End()
+	}()
+
 	n, err = f.f.ReadAt(ctx, p, off)
 	if err != nil {
 		f.log.Error("Failed to read", "offset", off, "error", err)
@@ -130,8 +277,8 @@ func (f *LogFile) Size() int64 {
 }
 
 // Stat implements File.
-func (f *LogFile) Stat() (fs.FileInfo, error) {
-	info, err := f.f.Stat()
+func (f *LogFile) Info() (fs.FileInfo, error) {
+	info, err := f.f.Info()
 	if err != nil {
 		f.log.Error("Failed to read", "error", err)
 	}
diff --git a/src/host/vfs/memory.go b/src/host/vfs/memory.go
index 79d2f0e..8f4f84f 100644
--- a/src/host/vfs/memory.go
+++ b/src/host/vfs/memory.go
@@ -5,15 +5,41 @@ import (
 	"context"
 	"io/fs"
 	"path"
+	"time"
 )
 
-var _ Filesystem = &MemoryFs{}
-
 type MemoryFs struct {
 	name  string
 	files map[string]*MemoryFile
 }
 
+var _ Filesystem = (*MemoryFs)(nil)
+
+// ModTime implements Filesystem.
+func (mfs *MemoryFs) ModTime() time.Time {
+	return time.Time{}
+}
+
+// Mode implements Filesystem.
+func (mfs *MemoryFs) Mode() fs.FileMode {
+	return fs.ModeDir
+}
+
+// Size implements Filesystem.
+func (fs *MemoryFs) Size() int64 {
+	return 0
+}
+
+// Sys implements Filesystem.
+func (fs *MemoryFs) Sys() any {
+	return nil
+}
+
+// FsKind implements Filesystem.
+func (fs *MemoryFs) FsName() string {
+	return "memoryfs"
+}
+
 // Info implements Filesystem.
 func (fs *MemoryFs) Info() (fs.FileInfo, error) {
 	return newDirInfo(fs.name), nil
@@ -77,7 +103,17 @@ func NewMemoryFile(name string, data []byte) *MemoryFile {
 	}
 }
 
-func (d *MemoryFile) Stat() (fs.FileInfo, error) {
+// Name implements File.
+func (d *MemoryFile) Name() string {
+	return d.name
+}
+
+// Type implements File.
+func (d *MemoryFile) Type() fs.FileMode {
+	return roMode
+}
+
+func (d *MemoryFile) Info() (fs.FileInfo, error) {
 	return newFileInfo(d.name, int64(d.data.Len())), nil
 }
 
diff --git a/src/host/vfs/os.go b/src/host/vfs/os.go
index 18daf11..27e8f1a 100644
--- a/src/host/vfs/os.go
+++ b/src/host/vfs/os.go
@@ -12,13 +12,19 @@ type OsFS struct {
 	hostDir string
 }
 
+var _ Filesystem = (*OsFS)(nil)
+
 // Stat implements Filesystem.
 func (fs *OsFS) Stat(ctx context.Context, filename string) (fs.FileInfo, error) {
 	if path.Clean(filename) == Separator {
 		return newDirInfo(Separator), nil
 	}
 
-	return os.Stat(path.Join(fs.hostDir, filename))
+	info, err := os.Stat(path.Join(fs.hostDir, filename))
+	if err != nil {
+		return nil, err
+	}
+	return info, nil
 }
 
 // Unlink implements Filesystem.
@@ -28,11 +34,11 @@ 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 path.Clean(filename) == Separator {
-		return NewDir(filename), nil
+	if isRoot(filename) {
+		return newDirFile(fs.Name()), nil
 	}
 
-	return NewLazyOsFile(path.Join(fs.hostDir, filename)), nil
+	return NewLazyOsFile(path.Join(fs.hostDir, filename))
 }
 
 // ReadDir implements Filesystem.
@@ -42,7 +48,7 @@ func (o *OsFS) ReadDir(ctx context.Context, dir string) ([]fs.DirEntry, error) {
 
 // Info implements Filesystem.
 func (fs *OsFS) Info() (fs.FileInfo, error) {
-	return newDirInfo(path.Base(fs.hostDir)), nil
+	return newDirInfo(fs.Name()), nil
 }
 
 // IsDir implements Filesystem.
@@ -68,56 +74,16 @@ func NewOsFs(osDir string) *OsFS {
 
 var _ Filesystem = &OsFS{}
 
-type OsFile struct {
-	f *os.File
-}
-
-func NewOsFile(f *os.File) *OsFile {
-	return &OsFile{f: f}
-}
-
-var _ File = &OsFile{}
-
-// Info implements File.
-func (f *OsFile) Info() (fs.FileInfo, error) {
-	return f.f.Stat()
-}
-
-// Close implements File.
-func (f *OsFile) Close(ctx context.Context) error {
-	return f.f.Close()
-}
-
-// Read implements File.
-func (f *OsFile) Read(ctx context.Context, p []byte) (n int, err error) {
-	return f.f.Read(p)
-}
-
-// ReadAt implements File.
-func (f *OsFile) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) {
-	return f.f.ReadAt(p, off)
-}
-
-func (f *OsFile) Stat() (fs.FileInfo, error) {
-	return f.f.Stat()
-}
-
-// Size implements File.
-func (f *OsFile) Size() int64 {
-	stat, err := f.Stat()
+func NewLazyOsFile(path string) (*LazyOsFile, error) {
+	info, err := os.Stat(path)
 	if err != nil {
-		return 0
+		return nil, err
 	}
-	return stat.Size()
-}
 
-// IsDir implements File.
-func (f *OsFile) IsDir() bool {
-	stat, err := f.Stat()
-	if err != nil {
-		return false
-	}
-	return stat.IsDir()
+	return &LazyOsFile{
+		path: path,
+		info: info,
+	}, nil
 }
 
 type LazyOsFile struct {
@@ -125,15 +91,10 @@ type LazyOsFile struct {
 	path string
 	file *os.File
 
-	// cached field
 	info fs.FileInfo
 }
 
-func NewLazyOsFile(path string) *LazyOsFile {
-	return &LazyOsFile{path: path}
-}
-
-var _ File = &OsFile{}
+var _ File = (*LazyOsFile)(nil)
 
 func (f *LazyOsFile) open() error {
 	f.m.Lock()
@@ -151,6 +112,16 @@ func (f *LazyOsFile) open() error {
 	return nil
 }
 
+// Name implements File.
+func (f *LazyOsFile) Name() string {
+	return path.Base(f.path)
+}
+
+// Type implements File.
+func (f *LazyOsFile) Type() fs.FileMode {
+	return f.info.Mode()
+}
+
 // Close implements File.
 func (f *LazyOsFile) Close(ctx context.Context) error {
 	if f.file == nil {
@@ -177,41 +148,17 @@ func (f *LazyOsFile) ReadAt(ctx context.Context, p []byte, off int64) (n int, er
 	return f.file.ReadAt(p, off)
 }
 
-func (f *LazyOsFile) Stat() (fs.FileInfo, error) {
-	f.m.Lock()
-	defer f.m.Unlock()
-	if f.info == nil {
-		if f.file == nil {
-			info, err := os.Stat(f.path)
-			if err != nil {
-				return nil, err
-			}
-			f.info = info
-		} else {
-			info, err := f.file.Stat()
-			if err != nil {
-				return nil, err
-			}
-			f.info = info
-		}
-	}
+func (f *LazyOsFile) Info() (fs.FileInfo, error) {
+
 	return f.info, nil
 }
 
 // Size implements File.
 func (f *LazyOsFile) Size() int64 {
-	stat, err := f.Stat()
-	if err != nil {
-		return 0
-	}
-	return stat.Size()
+	return f.info.Size()
 }
 
 // IsDir implements File.
 func (f *LazyOsFile) IsDir() bool {
-	stat, err := f.Stat()
-	if err != nil {
-		return false
-	}
-	return stat.IsDir()
+	return f.info.IsDir()
 }
diff --git a/src/host/vfs/os_test.go b/src/host/vfs/os_test.go
new file mode 100644
index 0000000..06f26e3
--- /dev/null
+++ b/src/host/vfs/os_test.go
@@ -0,0 +1,75 @@
+package vfs_test
+
+import (
+	"context"
+	"os"
+	"testing"
+
+	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
+	"github.com/stretchr/testify/require"
+)
+
+func TestOsFs(t *testing.T) {
+	const testDir = "/tmp/tstor-test"
+	require := require.New(t)
+	ctx := context.Background()
+
+	err := os.RemoveAll(testDir)
+	require.NotErrorIs(err, os.ErrNotExist)
+	defer func() {
+		err = os.RemoveAll(testDir)
+		require.NotErrorIs(err, os.ErrNotExist)
+	}()
+
+	err = os.MkdirAll(testDir, os.ModePerm)
+	require.NoError(err)
+
+	err = os.MkdirAll(testDir+"/dir1", os.ModePerm)
+	require.NoError(err)
+	err = os.MkdirAll(testDir+"/dir1/dir2", os.ModePerm)
+	require.NoError(err)
+	err = os.MkdirAll(testDir+"/dir1/dir3", os.ModePerm)
+	require.NoError(err)
+	osfile, err := os.Create(testDir + "/dir1/dir2/file")
+	require.NoError(err)
+	err = osfile.Close()
+	require.NoError(err)
+
+	fs := vfs.NewOsFs(testDir)
+
+	dirs := []string{"/", "/.", "/dir1", "/dir1/dir2"}
+
+	for _, dir := range dirs {
+		file, err := fs.Open(ctx, dir)
+		require.NoError(err)
+		require.True(file.IsDir())
+		stat, err := file.Info()
+		require.NoError(err)
+		require.True(stat.IsDir())
+		require.NoError(file.Close(ctx))
+
+		info, err := fs.Stat(ctx, dir)
+		require.NoError(err)
+		require.True(info.IsDir())
+
+		entries, err := fs.ReadDir(ctx, dir)
+		require.NoError(err)
+
+		for _, e := range entries {
+			switch e.Name() {
+			case "dir2", "dir1", "dir3":
+				require.False(e.Type().IsRegular())
+				require.True(e.Type().IsDir())
+				require.True(e.IsDir())
+			case "file":
+				require.True(e.Type().IsRegular())
+				require.False(e.Type().IsDir())
+				require.False(e.IsDir())
+			}
+		}
+	}
+
+	file, err := fs.Open(ctx, "/dir1/dir2/file")
+	require.NoError(err)
+	require.False(file.IsDir())
+}
diff --git a/src/host/vfs/resolver.go b/src/host/vfs/resolver.go
index d434099..6573ab5 100644
--- a/src/host/vfs/resolver.go
+++ b/src/host/vfs/resolver.go
@@ -2,28 +2,84 @@ package vfs
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"io/fs"
+	"log/slog"
 	"path"
+	"reflect"
 	"slices"
 	"strings"
 	"sync"
+	"time"
+
+	"git.kmsign.ru/royalcat/tstor/pkg/rlog"
+	"go.opentelemetry.io/otel/attribute"
+	"go.opentelemetry.io/otel/trace"
+	"golang.org/x/exp/maps"
 )
 
 type ResolverFS struct {
 	rootFS   Filesystem
 	resolver *resolver
+
+	log *slog.Logger
 }
 
 func NewResolveFS(rootFs Filesystem, factories map[string]FsFactory) *ResolverFS {
 	return &ResolverFS{
 		rootFS:   rootFs,
 		resolver: newResolver(factories),
+		log:      rlog.ComponentLog("fs/resolverfs"),
 	}
 }
 
+// ModTime implements Filesystem.
+func (r *ResolverFS) ModTime() time.Time {
+	return time.Time{}
+}
+
+// Mode implements Filesystem.
+func (r *ResolverFS) Mode() fs.FileMode {
+	return fs.ModeDir
+}
+
+// Size implements Filesystem.
+func (r *ResolverFS) Size() int64 {
+	return 0
+}
+
+// Sys implements Filesystem.
+func (r *ResolverFS) Sys() any {
+	return nil
+}
+
+// FsName implements Filesystem.
+func (r *ResolverFS) FsName() string {
+	return "resolverfs"
+}
+
+func (fs *ResolverFS) traceAttrs(add ...attribute.KeyValue) trace.SpanStartOption {
+	return trace.WithAttributes(append([]attribute.KeyValue{
+		attribute.String("fs", fs.FsName()),
+	}, add...)...)
+}
+
+func (r *ResolverFS) ResolvablesExtensions() []string {
+	return maps.Keys(r.resolver.factories)
+}
+
 // Open implements Filesystem.
 func (r *ResolverFS) Open(ctx context.Context, filename string) (File, error) {
+	ctx, span := tracer.Start(ctx, "Open",
+		r.traceAttrs(attribute.String("filename", filename)),
+	)
+	defer span.End()
+
+	if path.Clean(filename) == Separator {
+		return newDirFile(r.Name()), nil
+	}
+
 	fsPath, nestedFs, nestedFsPath, err := r.resolver.resolvePath(ctx, filename, r.rootFS.Open)
 	if err != nil {
 		return nil, err
@@ -37,6 +93,11 @@ func (r *ResolverFS) Open(ctx context.Context, filename string) (File, error) {
 
 // ReadDir implements Filesystem.
 func (r *ResolverFS) ReadDir(ctx context.Context, dir string) ([]fs.DirEntry, error) {
+	ctx, span := tracer.Start(ctx, "ReadDir",
+		r.traceAttrs(attribute.String("name", dir)),
+	)
+	defer span.End()
+
 	fsPath, nestedFs, nestedFsPath, err := r.resolver.resolvePath(ctx, dir, r.rootFS.Open)
 	if err != nil {
 		return nil, err
@@ -57,8 +118,14 @@ func (r *ResolverFS) ReadDir(ctx context.Context, dir string) ([]fs.DirEntry, er
 			if err != nil {
 				return nil, err
 			}
+			defer file.Close(ctx)
 			nestedfs, err := r.resolver.nestedFs(ctx, filepath, file)
 			if err != nil {
+				if errors.Is(err, context.DeadlineExceeded) {
+					r.log.ErrorContext(ctx, "creating fs timed out", "filename", e.Name())
+					continue
+				}
+
 				return nil, err
 			}
 
@@ -72,11 +139,23 @@ func (r *ResolverFS) ReadDir(ctx context.Context, dir string) ([]fs.DirEntry, er
 
 // Stat implements Filesystem.
 func (r *ResolverFS) Stat(ctx context.Context, filename string) (fs.FileInfo, error) {
+	ctx, span := tracer.Start(ctx, "Stat",
+		r.traceAttrs(attribute.String("filename", filename)),
+	)
+	defer span.End()
+
+	if isRoot(filename) {
+		return r, nil
+	}
+
 	fsPath, nestedFs, nestedFsPath, err := r.resolver.resolvePath(ctx, filename, r.rootFS.Open)
 	if err != nil {
 		return nil, err
 	}
+	span.SetAttributes(attribute.String("fsPath", fsPath), attribute.String("nestedFsPath", nestedFsPath))
+
 	if nestedFs != nil {
+		span.AddEvent("calling nested fs")
 		return nestedFs.Stat(ctx, nestedFsPath)
 	}
 
@@ -98,7 +177,7 @@ func (r *ResolverFS) Unlink(ctx context.Context, filename string) error {
 
 // Info implements Filesystem.
 func (r *ResolverFS) Info() (fs.FileInfo, error) {
-	return newDirInfo(r.rootFS.Name()), nil
+	return r, nil
 }
 
 // IsDir implements Filesystem.
@@ -108,7 +187,7 @@ func (r *ResolverFS) IsDir() bool {
 
 // Name implements Filesystem.
 func (r *ResolverFS) Name() string {
-	return r.Name()
+	return r.rootFS.Name()
 }
 
 // Type implements Filesystem.
@@ -120,8 +199,6 @@ var _ Filesystem = &ResolverFS{}
 
 type FsFactory func(ctx context.Context, f File) (Filesystem, error)
 
-const Separator = "/"
-
 func newResolver(factories map[string]FsFactory) *resolver {
 	return &resolver{
 		factories: factories,
@@ -171,6 +248,9 @@ 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) {
+	ctx, span := tracer.Start(ctx, "resolvePath")
+	defer span.End()
+
 	name = path.Clean(name)
 	name = strings.TrimPrefix(name, Separator)
 	parts := strings.Split(name, Separator)
@@ -205,8 +285,12 @@ PARTS_LOOP:
 	defer r.m.Unlock()
 
 	if nestedFs, ok := r.fsmap[fsPath]; ok {
+		span.AddEvent("fs loaded from cache", trace.WithAttributes(attribute.String("nestedFs", reflect.TypeOf(nestedFs).Name())))
 		return fsPath, nestedFs, nestedFsPath, nil
 	} else {
+		ctx, span := tracer.Start(ctx, "CreateFS")
+		defer span.End()
+
 		fsFile, err := rawOpen(ctx, fsPath)
 		if err != nil {
 			return "", nil, "", fmt.Errorf("error opening filesystem file: %s with error: %w", fsPath, err)
@@ -217,6 +301,8 @@ PARTS_LOOP:
 		}
 		r.fsmap[fsPath] = nestedFs
 
+		span.AddEvent("fs created", trace.WithAttributes(attribute.String("nestedFs", reflect.TypeOf(nestedFs).Name())))
+
 		return fsPath, nestedFs, nestedFsPath, nil
 	}
 
@@ -226,7 +312,7 @@ var ErrNotExist = fs.ErrNotExist
 
 func getFile[F File](m map[string]F, name string) (File, error) {
 	if name == Separator {
-		return NewDir(name), nil
+		return newDirFile(name), nil
 	}
 
 	f, ok := m[name]
@@ -236,7 +322,7 @@ func getFile[F File](m map[string]F, name string) (File, error) {
 
 	for p := range m {
 		if strings.HasPrefix(p, name) {
-			return NewDir(name), nil
+			return newDirFile(name), nil
 		}
 	}
 
diff --git a/src/host/vfs/resolver_test.go b/src/host/vfs/resolver_test.go
index ab801f9..4b927fd 100644
--- a/src/host/vfs/resolver_test.go
+++ b/src/host/vfs/resolver_test.go
@@ -1,235 +1,237 @@
-package vfs
+package vfs_test
 
 import (
+	"archive/zip"
+	"bytes"
 	"context"
-	"io/fs"
-	"os"
-	"path"
 	"testing"
 
+	"git.kmsign.ru/royalcat/tstor/src/host/vfs"
 	"github.com/stretchr/testify/require"
 )
 
-type Dummy struct {
-	name string
-}
+func createZip(files map[string][]byte) ([]byte, error) {
+	buf := bytes.NewBuffer(nil)
+	zw := zip.NewWriter(buf)
 
-// Stat implements File.
-func (d *Dummy) Stat() (fs.FileInfo, error) {
-	return newFileInfo(d.name, 0), nil
-}
+	for name, data := range files {
+		fw, err := zw.Create(name)
+		if err != nil {
+			return nil, err
+		}
 
-func (d *Dummy) Size() int64 {
-	return 0
-}
-
-func (d *Dummy) IsDir() bool {
-	return false
-}
-
-func (d *Dummy) Close(ctx context.Context) error {
-	return nil
-}
-
-func (d *Dummy) Read(ctx context.Context, p []byte) (n int, err error) {
-	return 0, nil
-}
-
-func (d *Dummy) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) {
-	return 0, nil
-}
-
-var _ File = &Dummy{}
-
-type DummyFs struct {
-	name string
-}
-
-// Stat implements Filesystem.
-func (*DummyFs) Stat(ctx context.Context, filename string) (fs.FileInfo, error) {
-	return newFileInfo(path.Base(filename), 0), nil // TODO
-}
-
-func (d *DummyFs) Open(ctx context.Context, filename string) (File, error) {
-	return &Dummy{}, nil
-}
-
-func (d *DummyFs) Unlink(ctx context.Context, filename string) error {
-	return ErrNotImplemented
-}
-
-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),
-		}, nil
+		_, err = fw.Write(data)
+		if err != nil {
+			return nil, err
+		}
+	}
+	err := zw.Flush()
+	if err != nil {
+		return nil, err
 	}
 
-	return nil, os.ErrNotExist
+	err = zw.Close()
+	if err != nil {
+		return nil, err
+	}
+
+	return buf.Bytes(), nil
 }
 
-// Info implements Filesystem.
-func (d *DummyFs) Info() (fs.FileInfo, error) {
-	return newDirInfo(d.name), nil
-}
-
-// IsDir implements Filesystem.
-func (d *DummyFs) IsDir() bool {
-	return true
-}
-
-// Name implements Filesystem.
-func (d *DummyFs) Name() string {
-	return d.name
-}
-
-// Type implements Filesystem.
-func (d *DummyFs) Type() fs.FileMode {
-	return fs.ModeDir
-}
-
-var _ Filesystem = &DummyFs{}
-
-func TestResolver(t *testing.T) {
+func TestResolverFs(t *testing.T) {
 	t.Parallel()
-	resolver := newResolver(ArchiveFactories)
 	ctx := context.Background()
 
-	t.Run("nested fs", func(t *testing.T) {
-		t.Parallel()
-		require := require.New(t)
-
-		fsPath, nestedFs, nestedFsPath, err := resolver.resolvePath(ctx, "/f1.rar/f2.rar", func(_ context.Context, path string) (File, error) {
-			require.Equal("/f1.rar", path)
-			return &Dummy{}, nil
-		})
-		require.NoError(err)
-		require.Equal("/f1.rar", fsPath)
-		require.Equal("/f2.rar", nestedFsPath)
-		require.IsType(&ArchiveFS{}, nestedFs)
+	testZip, err := createZip(map[string][]byte{
+		"123.txt":       []byte("123"),
+		"files/321.txt": []byte("321"),
 	})
-	t.Run("root", func(t *testing.T) {
+	require.NoError(t, err)
+
+	fs := vfs.NewResolveFS(vfs.NewMemoryFS("/", map[string]*vfs.MemoryFile{
+		"/data/123.zip": vfs.NewMemoryFile("123.zip", testZip),
+	}), vfs.ArchiveFactories)
+
+	t.Run("dir", func(t *testing.T) {
 		t.Parallel()
 		require := require.New(t)
 
-		fsPath, nestedFs, nestedFsPath, err := resolver.resolvePath(ctx, "/", func(_ context.Context, path string) (File, error) {
-			require.Equal("/", path)
-			return &Dummy{}, nil
-		})
-		require.NoError(err)
-		require.Nil(nestedFs)
-		require.Equal("/", fsPath)
-		require.Equal("", nestedFsPath)
-	})
+		dirs := []string{
+			"/data", "/", "/.",
+			"/data/123.zip", "/data/123.zip/files", "/data/123.zip/files/.",
+		}
 
-	t.Run("root dirty", func(t *testing.T) {
-		t.Parallel()
-		require := require.New(t)
+		for _, dir := range dirs {
+			file, err := fs.Open(ctx, dir)
+			require.NoError(err)
+			require.True(file.IsDir())
 
-		fsPath, nestedFs, nestedFsPath, err := resolver.resolvePath(ctx, "//.//", func(_ context.Context, path string) (File, error) {
-			require.Equal("/", path)
-			return &Dummy{}, nil
-		})
-		require.NoError(err)
-		require.Nil(nestedFs)
-		require.Equal("/", fsPath)
-		require.Equal("", nestedFsPath)
-	})
-	t.Run("fs dirty", func(t *testing.T) {
-		t.Parallel()
-		require := require.New(t)
+			stat, err := file.Info()
+			require.NoError(err)
+			require.True(stat.IsDir())
+		}
 
-		fsPath, nestedFs, nestedFsPath, err := resolver.resolvePath(ctx, "//.//f1.rar", func(_ context.Context, path string) (File, error) {
-			require.Equal("/f1.rar", path)
-			return &Dummy{}, nil
-		})
+		entries, err := fs.ReadDir(ctx, "/data")
 		require.NoError(err)
-		require.Equal("/f1.rar", fsPath)
-		require.Equal("/", nestedFsPath)
-		require.IsType(&ArchiveFS{}, nestedFs)
-	})
-	t.Run("inside folder", func(t *testing.T) {
-		t.Parallel()
-		require := require.New(t)
+		require.Len(entries, 1)
 
-		fsPath, nestedFs, nestedFsPath, err := resolver.resolvePath(ctx, "//test1/f1.rar", func(_ context.Context, path string) (File, error) {
-			require.Equal("/test1/f1.rar", path)
-			return &Dummy{}, nil
-		})
+		for _, e := range entries {
+			switch e.Name() {
+			case "123.zip":
+				require.True(e.IsDir())
+				require.IsType(&vfs.ArchiveFS{}, e)
+			}
+		}
+
+		entries, err = fs.ReadDir(ctx, "/data/123.zip/files")
 		require.NoError(err)
-		require.IsType(&ArchiveFS{}, nestedFs)
-		require.Equal("/test1/f1.rar", fsPath)
-		require.Equal("/", nestedFsPath)
+		require.Len(entries, 1)
+
+		entries, err = fs.ReadDir(ctx, "/data/123.zip")
+		require.NoError(err)
+		require.Len(entries, 3)
+
+		for _, e := range entries {
+			switch e.Name() {
+			case "files":
+				require.True(e.IsDir())
+			case "123.txt":
+				require.False(e.IsDir())
+			}
+		}
 	})
 }
 
-func TestArchiveFactories(t *testing.T) {
-	t.Parallel()
+// func TestResolver(t *testing.T) {
+// 	t.Parallel()
+// 	resolver := newResolver(ArchiveFactories)
+// 	ctx := context.Background()
 
-	ctx := context.Background()
+// 	t.Run("nested fs", func(t *testing.T) {
+// 		t.Parallel()
+// 		require := require.New(t)
 
-	require := require.New(t)
+// 		fsPath, nestedFs, nestedFsPath, err := resolver.resolvePath(ctx, "/f1.rar/f2.rar", func(_ context.Context, path string) (File, error) {
+// 			require.Equal("/f1.rar", path)
+// 			return &vfs.Dummy{}, nil
+// 		})
+// 		require.NoError(err)
+// 		require.Equal("/f1.rar", fsPath)
+// 		require.Equal("/f2.rar", nestedFsPath)
+// 		require.IsType(&vfs.ArchiveFS{}, nestedFs)
+// 	})
+// 	t.Run("root", func(t *testing.T) {
+// 		t.Parallel()
+// 		require := require.New(t)
 
-	require.Contains(ArchiveFactories, ".zip")
-	require.Contains(ArchiveFactories, ".rar")
-	require.Contains(ArchiveFactories, ".7z")
+// 		fsPath, nestedFs, nestedFsPath, err := resolver.resolvePath(ctx, "/", func(_ context.Context, path string) (File, error) {
+// 			require.Equal("/", path)
+// 			return &Dummy{}, nil
+// 		})
+// 		require.NoError(err)
+// 		require.Nil(nestedFs)
+// 		require.Equal("/", fsPath)
+// 		require.Equal("", nestedFsPath)
+// 	})
 
-	fs, err := ArchiveFactories[".zip"](ctx, &Dummy{})
-	require.NoError(err)
-	require.NotNil(fs)
+// 	t.Run("root dirty", func(t *testing.T) {
+// 		t.Parallel()
+// 		require := require.New(t)
 
-	fs, err = ArchiveFactories[".rar"](ctx, &Dummy{})
-	require.NoError(err)
-	require.NotNil(fs)
+// 		fsPath, nestedFs, nestedFsPath, err := resolver.resolvePath(ctx, "//.//", func(_ context.Context, path string) (File, error) {
+// 			require.Equal("/", path)
+// 			return &Dummy{}, nil
+// 		})
+// 		require.NoError(err)
+// 		require.Nil(nestedFs)
+// 		require.Equal("/", fsPath)
+// 		require.Equal("", nestedFsPath)
+// 	})
 
-	fs, err = ArchiveFactories[".7z"](ctx, &Dummy{})
-	require.NoError(err)
-	require.NotNil(fs)
-}
+// 	t.Run("root dirty 2", func(t *testing.T) {
+// 		t.Parallel()
+// 		require := require.New(t)
 
-func TestFiles(t *testing.T) {
-	t.Parallel()
-	require := require.New(t)
+// 		fsPath, nestedFs, nestedFsPath, err := resolver.resolvePath(ctx, "/.", func(_ context.Context, path string) (File, error) {
+// 			require.Equal("/", path)
+// 			return &Dummy{}, nil
+// 		})
+// 		require.NoError(err)
+// 		require.Nil(nestedFs)
+// 		require.Equal("/", fsPath)
+// 		require.Equal("", nestedFsPath)
+// 	})
 
-	files := map[string]*Dummy{
-		"/test/file.txt":  &Dummy{},
-		"/test/file2.txt": &Dummy{},
-		"/test1/file.txt": &Dummy{},
-	}
-	{
-		file, err := getFile(files, "/test")
-		require.NoError(err)
-		require.Equal(&dir{name: "test"}, file)
-	}
-	{
-		file, err := getFile(files, "/test/file.txt")
-		require.NoError(err)
-		require.Equal(&Dummy{}, file)
-	}
-	{
-		out, err := listDirFromFiles(files, "/test")
-		require.NoError(err)
-		require.Len(out, 2)
-		require.Equal("file.txt", out[0].Name())
-		require.Equal("file2.txt", out[1].Name())
-		require.False(out[0].IsDir())
-		require.False(out[1].IsDir())
-	}
-	{
-		out, err := listDirFromFiles(files, "/test1")
-		require.NoError(err)
-		require.Len(out, 1)
-		require.Equal("file.txt", out[0].Name())
-		require.False(out[0].IsDir())
-	}
-	{
-		out, err := listDirFromFiles(files, "/")
-		require.NoError(err)
-		require.Len(out, 2)
-		require.Equal("test", out[0].Name())
-		require.Equal("test1", out[1].Name())
-		require.True(out[0].IsDir())
-		require.True(out[1].IsDir())
-	}
-}
+// 	t.Run("fs dirty", func(t *testing.T) {
+// 		t.Parallel()
+// 		require := require.New(t)
+
+// 		fsPath, nestedFs, nestedFsPath, err := resolver.resolvePath(ctx, "//.//f1.rar", func(_ context.Context, path string) (File, error) {
+// 			require.Equal("/f1.rar", path)
+// 			return &Dummy{}, nil
+// 		})
+// 		require.NoError(err)
+// 		require.Equal("/f1.rar", fsPath)
+// 		require.Equal("/", nestedFsPath)
+// 		require.IsType(&ArchiveFS{}, nestedFs)
+// 	})
+// 	t.Run("inside folder", func(t *testing.T) {
+// 		t.Parallel()
+// 		require := require.New(t)
+
+// 		fsPath, nestedFs, nestedFsPath, err := resolver.resolvePath(ctx, "//test1/f1.rar", func(_ context.Context, path string) (File, error) {
+// 			require.Equal("/test1/f1.rar", path)
+// 			return &Dummy{}, nil
+// 		})
+// 		require.NoError(err)
+// 		require.IsType(&ArchiveFS{}, nestedFs)
+// 		require.Equal("/test1/f1.rar", fsPath)
+// 		require.Equal("/", nestedFsPath)
+// 	})
+// }
+
+// func TestFiles(t *testing.T) {
+// 	t.Parallel()
+// 	require := require.New(t)
+
+// 	files := map[string]*vfs.DummyFile{
+// 		"/test/file.txt":  &vfs.DummyFile{},
+// 		"/test/file2.txt": &vfs.DummyFile{},
+// 		"/test1/file.txt": &vfs.DummyFile{},
+// 	}
+// 	{
+// 		file, err := getFile(files, "/test")
+// 		require.NoError(err)
+// 		require.Equal(&dir{name: "test"}, file)
+// 	}
+// 	{
+// 		file, err := getFile(files, "/test/file.txt")
+// 		require.NoError(err)
+// 		require.Equal(&Dummy{}, file)
+// 	}
+// 	{
+// 		out, err := listDirFromFiles(files, "/test")
+// 		require.NoError(err)
+// 		require.Len(out, 2)
+// 		require.Equal("file.txt", out[0].Name())
+// 		require.Equal("file2.txt", out[1].Name())
+// 		require.False(out[0].IsDir())
+// 		require.False(out[1].IsDir())
+// 	}
+// 	{
+// 		out, err := listDirFromFiles(files, "/test1")
+// 		require.NoError(err)
+// 		require.Len(out, 1)
+// 		require.Equal("file.txt", out[0].Name())
+// 		require.False(out[0].IsDir())
+// 	}
+// 	{
+// 		out, err := listDirFromFiles(files, "/")
+// 		require.NoError(err)
+// 		require.Len(out, 2)
+// 		require.Equal("test", out[0].Name())
+// 		require.Equal("test1", out[1].Name())
+// 		require.True(out[0].IsDir())
+// 		require.True(out[1].IsDir())
+// 	}
+// }
diff --git a/src/host/vfs/torrent.go b/src/host/vfs/torrent.go
index 7f4545b..f33c326 100644
--- a/src/host/vfs/torrent.go
+++ b/src/host/vfs/torrent.go
@@ -8,15 +8,15 @@ import (
 	"slices"
 	"strings"
 	"sync"
+	"time"
 
-	"git.kmsign.ru/royalcat/tstor/pkg/ctxio"
 	"git.kmsign.ru/royalcat/tstor/src/host/controller"
 	"github.com/anacrolix/torrent"
+	"go.opentelemetry.io/otel/attribute"
+	"go.opentelemetry.io/otel/trace"
 	"golang.org/x/exp/maps"
 )
 
-var _ Filesystem = &TorrentFs{}
-
 type TorrentFs struct {
 	name string
 
@@ -28,6 +28,8 @@ type TorrentFs struct {
 	resolver *resolver
 }
 
+var _ Filesystem = (*TorrentFs)(nil)
+
 func NewTorrentFs(name string, c *controller.Torrent) *TorrentFs {
 	return &TorrentFs{
 		name:     name,
@@ -45,7 +47,7 @@ func (tfs *TorrentFs) Name() string {
 
 // Info implements fs.DirEntry.
 func (tfs *TorrentFs) Info() (fs.FileInfo, error) {
-	return newDirInfo(tfs.name), nil
+	return tfs, nil
 }
 
 // IsDir implements fs.DirEntry.
@@ -58,6 +60,31 @@ func (tfs *TorrentFs) Type() fs.FileMode {
 	return fs.ModeDir
 }
 
+// ModTime implements fs.FileInfo.
+func (tfs *TorrentFs) ModTime() time.Time {
+	return time.Time{}
+}
+
+// Mode implements fs.FileInfo.
+func (tfs *TorrentFs) Mode() fs.FileMode {
+	return fs.ModeDir
+}
+
+// Size implements fs.FileInfo.
+func (tfs *TorrentFs) Size() int64 {
+	return 0
+}
+
+// Sys implements fs.FileInfo.
+func (tfs *TorrentFs) Sys() any {
+	return nil
+}
+
+// FsName implements Filesystem.
+func (tfs *TorrentFs) FsName() string {
+	return "torrentfs"
+}
+
 func (fs *TorrentFs) files(ctx context.Context) (map[string]File, error) {
 	fs.mu.Lock()
 	defer fs.mu.Unlock()
@@ -66,14 +93,17 @@ func (fs *TorrentFs) files(ctx context.Context) (map[string]File, error) {
 		return fs.filesCache, nil
 	}
 
-	files, err := fs.Torrent.Files(context.Background())
+	ctx, span := tracer.Start(ctx, "files", fs.traceAttrs())
+	defer span.End()
+
+	files, err := fs.Torrent.Files(ctx)
 	if err != nil {
 		return nil, err
 	}
 
 	fs.filesCache = make(map[string]File)
 	for _, file := range files {
-		file.Download()
+		file.SetPriority(torrent.PiecePriorityNormal)
 		p := AbsPath(file.Path())
 		tf, err := openTorrentFile(ctx, path.Base(p), file)
 		if err != nil {
@@ -93,7 +123,7 @@ func (fs *TorrentFs) files(ctx context.Context) (map[string]File, error) {
 			if nestedFs == nil {
 				goto DEFAULT_DIR // FIXME
 			}
-			fs.filesCache, err = listFilesRecursive(ctx, nestedFs, "/")
+			fs.filesCache, err = fs.listFilesRecursive(ctx, nestedFs, "/")
 			if err != nil {
 				return nil, err
 			}
@@ -141,7 +171,12 @@ DEFAULT_DIR:
 // 	return true
 // }
 
-func listFilesRecursive(ctx context.Context, vfs Filesystem, start string) (map[string]File, error) {
+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)
 	if err != nil {
@@ -150,7 +185,7 @@ func listFilesRecursive(ctx context.Context, vfs Filesystem, start string) (map[
 	for _, entry := range entries {
 		filename := path.Join(start, entry.Name())
 		if entry.IsDir() {
-			rec, err := listFilesRecursive(ctx, vfs, filename)
+			rec, err := fs.listFilesRecursive(ctx, vfs, filename)
 			if err != nil {
 				return nil, err
 			}
@@ -167,16 +202,31 @@ func listFilesRecursive(ctx context.Context, vfs Filesystem, start string) (map[
 	return out, nil
 }
 
-func (fs *TorrentFs) rawOpen(ctx context.Context, path string) (File, error) {
+func (fs *TorrentFs) rawOpen(ctx context.Context, filename string) (file File, err error) {
+	ctx, span := tracer.Start(ctx, "rawOpen",
+		fs.traceAttrs(attribute.String("filename", filename)),
+	)
+	defer func() {
+		if err != nil {
+			span.RecordError(err)
+		}
+		span.End()
+	}()
+
 	files, err := fs.files(ctx)
 	if err != nil {
 		return nil, err
 	}
-	file, err := getFile(files, path)
+	file, err = getFile(files, filename)
 	return file, err
 }
 
 func (fs *TorrentFs) rawStat(ctx context.Context, filename string) (fs.FileInfo, error) {
+	ctx, span := tracer.Start(ctx, "rawStat",
+		fs.traceAttrs(attribute.String("filename", filename)),
+	)
+	defer span.End()
+
 	files, err := fs.files(ctx)
 	if err != nil {
 		return nil, err
@@ -185,13 +235,26 @@ func (fs *TorrentFs) rawStat(ctx context.Context, filename string) (fs.FileInfo,
 	if err != nil {
 		return nil, err
 	}
-	return file.Stat()
+	return file.Info()
+}
+
+func (fs *TorrentFs) traceAttrs(add ...attribute.KeyValue) trace.SpanStartOption {
+	return trace.WithAttributes(append([]attribute.KeyValue{
+		attribute.String("fs", fs.FsName()),
+		attribute.String("torrent", fs.Torrent.Name()),
+		attribute.String("infohash", fs.Torrent.InfoHash()),
+	}, add...)...)
 }
 
 // Stat implements Filesystem.
 func (fs *TorrentFs) Stat(ctx context.Context, filename string) (fs.FileInfo, error) {
-	if filename == Separator {
-		return newDirInfo(filename), nil
+	ctx, span := tracer.Start(ctx, "Stat",
+		fs.traceAttrs(attribute.String("filename", filename)),
+	)
+	defer span.End()
+
+	if path.Clean(filename) == Separator {
+		return fs, nil
 	}
 
 	fsPath, nestedFs, nestedFsPath, err := fs.resolver.resolvePath(ctx, filename, fs.rawOpen)
@@ -206,6 +269,15 @@ func (fs *TorrentFs) Stat(ctx context.Context, filename string) (fs.FileInfo, er
 }
 
 func (fs *TorrentFs) Open(ctx context.Context, filename string) (File, error) {
+	ctx, span := tracer.Start(ctx, "Open",
+		fs.traceAttrs(attribute.String("filename", filename)),
+	)
+	defer span.End()
+
+	if path.Clean(filename) == Separator {
+		return newDirFile(fs.name), nil
+	}
+
 	fsPath, nestedFs, nestedFsPath, err := fs.resolver.resolvePath(ctx, filename, fs.rawOpen)
 	if err != nil {
 		return nil, err
@@ -218,6 +290,11 @@ func (fs *TorrentFs) Open(ctx context.Context, filename string) (File, error) {
 }
 
 func (fs *TorrentFs) ReadDir(ctx context.Context, name string) ([]fs.DirEntry, error) {
+	ctx, span := tracer.Start(ctx, "ReadDir",
+		fs.traceAttrs(attribute.String("name", name)),
+	)
+	defer span.End()
+
 	fsPath, nestedFs, nestedFsPath, err := fs.resolver.resolvePath(ctx, name, fs.rawOpen)
 	if err != nil {
 		return nil, err
@@ -234,6 +311,11 @@ func (fs *TorrentFs) ReadDir(ctx context.Context, name string) ([]fs.DirEntry, e
 }
 
 func (fs *TorrentFs) Unlink(ctx context.Context, name string) error {
+	ctx, span := tracer.Start(ctx, "Unlink",
+		fs.traceAttrs(attribute.String("name", name)),
+	)
+	defer span.End()
+
 	name = AbsPath(name)
 
 	fs.mu.Lock()
@@ -256,10 +338,10 @@ func (fs *TorrentFs) Unlink(ctx context.Context, name string) error {
 		return ErrNotImplemented
 	}
 
-	return fs.Torrent.ExcludeFile(context.Background(), tfile.file)
+	return fs.Torrent.ExcludeFile(ctx, tfile.file)
 }
 
-var _ File = &torrentFile{}
+var _ File = (*torrentFile)(nil)
 
 type torrentFile struct {
 	name string
@@ -268,20 +350,24 @@ type torrentFile struct {
 
 	tr torrent.Reader
 
+	lastReadTimeout time.Time
+
 	file *torrent.File
 }
 
+const secondaryTimeout = time.Hour
+
 func openTorrentFile(ctx context.Context, name string, file *torrent.File) (*torrentFile, error) {
-	select {
-	case <-file.Torrent().GotInfo():
-		break
-	case <-ctx.Done():
-		return nil, ctx.Err()
-	}
+	// select {
+	// case <-file.Torrent().GotInfo():
+	// 	break
+	// case <-ctx.Done():
+	// 	return nil, ctx.Err()
+	// }
 
 	r := file.NewReader()
-	r.SetReadahead(4096) // TODO configurable
-	r.SetResponsive()
+	r.SetReadahead(1024 * 1024 * 16) // TODO configurable
+	// r.SetResponsive()
 
 	return &torrentFile{
 		name: name,
@@ -290,7 +376,17 @@ func openTorrentFile(ctx context.Context, name string, file *torrent.File) (*tor
 	}, nil
 }
 
-func (tf *torrentFile) Stat() (fs.FileInfo, error) {
+// Name implements File.
+func (tf *torrentFile) Name() string {
+	return tf.name
+}
+
+// Type implements File.
+func (tf *torrentFile) Type() fs.FileMode {
+	return roMode | fs.ModeDir
+}
+
+func (tf *torrentFile) Info() (fs.FileInfo, error) {
 	return newFileInfo(tf.name, tf.file.Length()), nil
 }
 
@@ -311,32 +407,80 @@ func (rw *torrentFile) Close(ctx context.Context) error {
 
 // Read implements ctxio.Reader.
 func (tf *torrentFile) Read(ctx context.Context, p []byte) (n int, err error) {
+	ctx, span := tracer.Start(ctx, "Read",
+		trace.WithAttributes(attribute.Int("length", len(p))),
+	)
+	defer func() {
+		span.SetAttributes(attribute.Int("read", n))
+		span.End()
+	}()
+
 	tf.mu.Lock()
 	defer tf.mu.Unlock()
 
+	if time.Since(tf.lastReadTimeout) < secondaryTimeout { // make short timeout for already faliled files
+		span.SetAttributes(attribute.Bool("short_timeout", true))
+		var cancel context.CancelFunc
+		ctx, cancel = context.WithTimeout(ctx, time.Millisecond)
+		defer cancel()
+	}
+	defer func() {
+		if err == context.DeadlineExceeded {
+			tf.lastReadTimeout = time.Now()
+		}
+	}()
+
 	return tf.tr.ReadContext(ctx, p)
 }
 
-func (yf *torrentFile) ReadAt(ctx context.Context, p []byte, off int64) (int, error) {
-	yf.mu.Lock()
-	defer yf.mu.Unlock()
+func (tf *torrentFile) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) {
+	ctx, span := tracer.Start(ctx, "ReadAt",
+		trace.WithAttributes(attribute.Int("length", len(p)), attribute.Int64("offset", off)),
+	)
+	defer func() {
+		span.SetAttributes(attribute.Int("read", n))
+		span.End()
+	}()
 
-	_, err := yf.tr.Seek(off, io.SeekStart)
+	tf.mu.Lock()
+	defer tf.mu.Unlock()
+
+	if time.Since(tf.lastReadTimeout) < secondaryTimeout { // make short timeout for already faliled files
+		span.SetAttributes(attribute.Bool("short_timeout", true))
+		var cancel context.CancelFunc
+		ctx, cancel = context.WithTimeout(ctx, time.Millisecond)
+		defer cancel()
+	}
+	defer func() {
+		if err == context.DeadlineExceeded {
+			tf.lastReadTimeout = time.Now()
+		}
+	}()
+
+	_, err = tf.tr.Seek(off, io.SeekStart)
 	if err != nil {
 		return 0, err
 	}
 
-	return readAtLeast(ctx, yf, p, len(p))
+	// return tf.tr.ReadContext(ctx, p)
+	n, err = readAtLeast(ctx, tf.tr, p, len(p))
+
+	_, err = tf.tr.Seek(0, io.SeekStart)
+	if err != nil {
+		return 0, err
+	}
+
+	return n, err
 }
 
-func readAtLeast(ctx context.Context, r ctxio.Reader, buf []byte, min int) (n int, err error) {
+func readAtLeast(ctx context.Context, r torrent.Reader, buf []byte, min int) (n int, err error) {
 	if len(buf) < min {
 		return 0, io.ErrShortBuffer
 	}
 	for n < min && err == nil {
 		var nn int
 
-		nn, err = r.Read(ctx, buf[n:])
+		nn, err = r.ReadContext(ctx, buf[n:])
 		n += nn
 	}
 	if n >= min {
diff --git a/src/host/vfs/torrent_test.go b/src/host/vfs/torrent_test.go
index 7de12ac..fcac812 100644
--- a/src/host/vfs/torrent_test.go
+++ b/src/host/vfs/torrent_test.go
@@ -1,13 +1,10 @@
 package vfs
 
 import (
-	"context"
 	"os"
 	"testing"
 
 	"github.com/anacrolix/torrent"
-
-	"github.com/stretchr/testify/require"
 )
 
 const testMagnet = "magnet:?xt=urn:btih:a88fda5954e89178c372716a6a78b8180ed4dad3&dn=The+WIRED+CD+-+Rip.+Sample.+Mash.+Share&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F&xs=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fwired-cd.torrent"
@@ -85,61 +82,61 @@ func TestMain(m *testing.M) {
 // 	require.NoError(f.Close())
 // }
 
-func TestReadAtTorrent(t *testing.T) {
-	t.Parallel()
+// func TestReadAtTorrent(t *testing.T) {
+// 	t.Parallel()
 
-	ctx := context.Background()
+// 	ctx := context.Background()
 
-	require := require.New(t)
+// 	require := require.New(t)
 
-	to, err := Cli.AddMagnet(testMagnet)
-	require.NoError(err)
+// 	to, err := Cli.AddMagnet(testMagnet)
+// 	require.NoError(err)
 
-	<-to.GotInfo()
-	torrFile := to.Files()[0]
+// 	<-to.GotInfo()
+// 	torrFile := to.Files()[0]
 
-	tf := torrentFile{
-		file: torrFile,
-	}
+// 	tf, err := openTorrentFile(ctx, "torr", torrFile)
+// 	require.NoError(err)
 
-	defer tf.Close(ctx)
+// 	defer tf.Close(ctx)
 
-	toRead := make([]byte, 5)
-	n, err := tf.ReadAt(ctx, toRead, 6)
-	require.NoError(err)
-	require.Equal(5, n)
-	require.Equal([]byte{0x0, 0x0, 0x1f, 0x76, 0x54}, toRead)
+// 	toRead := make([]byte, 5)
+// 	n, err := tf.ReadAt(ctx, toRead, 6)
+// 	require.NoError(err)
+// 	require.Equal(5, n)
+// 	require.Equal([]byte{0x0, 0x0, 0x1f, 0x76, 0x54}, toRead)
 
-	n, err = tf.ReadAt(ctx, toRead, 0)
-	require.NoError(err)
-	require.Equal(5, n)
-	require.Equal([]byte{0x49, 0x44, 0x33, 0x3, 0x0}, toRead)
-}
+// 	n, err = tf.ReadAt(ctx, toRead, 0)
+// 	require.NoError(err)
+// 	require.Equal(5, n)
+// 	require.Equal([]byte{0x49, 0x44, 0x33, 0x3, 0x0}, toRead)
+// }
 
-func TestReadAtWrapper(t *testing.T) {
-	t.Parallel()
+// func TestReadAtWrapper(t *testing.T) {
+// 	t.Parallel()
 
-	ctx := context.Background()
+// 	ctx := context.Background()
 
-	require := require.New(t)
+// 	require := require.New(t)
 
-	to, err := Cli.AddMagnet(testMagnet)
-	require.NoError(err)
+// 	to, err := Cli.AddMagnet(testMagnet)
+// 	require.NoError(err)
 
-	<-to.GotInfo()
-	torrFile := to.Files()[0]
+// 	<-to.GotInfo()
+// 	torrFile := to.Files()[0]
 
-	r, err := openTorrentFile(ctx, "file", torrFile)
-	defer r.Close(ctx)
+// 	r, err := openTorrentFile(ctx, "file", torrFile)
+// 	require.NoError(err)
+// 	defer r.Close(ctx)
 
-	toRead := make([]byte, 5)
-	n, err := r.ReadAt(ctx, toRead, 6)
-	require.NoError(err)
-	require.Equal(5, n)
-	require.Equal([]byte{0x0, 0x0, 0x1f, 0x76, 0x54}, toRead)
+// 	toRead := make([]byte, 5)
+// 	n, err := r.ReadAt(ctx, toRead, 6)
+// 	require.NoError(err)
+// 	require.Equal(5, n)
+// 	require.Equal([]byte{0x0, 0x0, 0x1f, 0x76, 0x54}, toRead)
 
-	n, err = r.ReadAt(ctx, toRead, 0)
-	require.NoError(err)
-	require.Equal(5, n)
-	require.Equal([]byte{0x49, 0x44, 0x33, 0x3, 0x0}, toRead)
-}
+// 	n, err = r.ReadAt(ctx, toRead, 0)
+// 	require.NoError(err)
+// 	require.Equal(5, n)
+// 	require.Equal([]byte{0x49, 0x44, 0x33, 0x3, 0x0}, toRead)
+// }
diff --git a/src/host/vfs/utils.go b/src/host/vfs/utils.go
index 799ed6f..0cfca90 100644
--- a/src/host/vfs/utils.go
+++ b/src/host/vfs/utils.go
@@ -1,10 +1,17 @@
 package vfs
 
 import (
+	"path"
 	"strings"
 	"sync"
 )
 
+const Separator = "/"
+
+func isRoot(filename string) bool {
+	return path.Clean(filename) == Separator
+}
+
 func trimRelPath(p, t string) string {
 	return strings.Trim(strings.TrimPrefix(p, t), "/")
 }
diff --git a/src/log/nfs.go b/src/log/nfs.go
index b48b46c..fb08422 100644
--- a/src/log/nfs.go
+++ b/src/log/nfs.go
@@ -5,22 +5,20 @@ import (
 	"log"
 	"log/slog"
 
-	nfs "github.com/willscott/go-nfs"
+	nfs "git.kmsign.ru/royalcat/tstor/pkg/go-nfs"
 )
 
 var _ nfs.Logger = (*NFSLog)(nil)
 
 type NFSLog struct {
 	level nfs.LogLevel
-	// r *slog.Logger
-	l *slog.Logger
+	l     *slog.Logger
 }
 
-func NewNFSLog(r *slog.Logger) nfs.Logger {
+func NewNFSLog(r *slog.Logger) *NFSLog {
 	return &NFSLog{
 		level: nfs.DebugLevel,
-		// l: r.Level(zerolog.DebugLevel),
-		l: r,
+		l:     r,
 	}
 }
 

From 1a1e658fa912d29b5f030577d2e25acf14d889d3 Mon Sep 17 00:00:00 2001
From: royalcat <k.adamovich20@gmail.com>
Date: Thu, 28 Mar 2024 16:18:28 +0300
Subject: [PATCH 18/18] [fix] ci

---
 .github/workflows/docker.yaml | 11 ++++-------
 1 file changed, 4 insertions(+), 7 deletions(-)

diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml
index f83b1cd..10e270f 100644
--- a/.github/workflows/docker.yaml
+++ b/.github/workflows/docker.yaml
@@ -28,16 +28,13 @@ jobs:
       - name: Checkout repository
         uses: actions/checkout@v3
 
-      - name: Set up QEMU
-        uses: docker/setup-qemu-action@v3
-
       - name: Set up Docker Buildx
         uses: docker/setup-buildx-action@v3
 
       - name: Login to Container Registry
         uses: docker/login-action@v3
         with:
-          registry: ghcr.io
+          registry: git.kmsign.ru
           username: ${{ github.actor }}
           password: ${{ secrets.GITHUB_TOKEN }}
 
@@ -46,7 +43,7 @@ jobs:
         uses: docker/metadata-action@v5
         with:
           # list of Docker images to use as base name for tags
-          images: ghcr.io/${{ github.repository }}
+          images: git.kmsign.ru/${{ github.repository }}
           # generate Docker tags based on the following events/attributes
           tags: |
             type=schedule
@@ -67,6 +64,6 @@ jobs:
           labels: ${{ steps.meta.outputs.labels }}
           sbom: true
           provenance: true
-          cache-from: type=gha
-          cache-to: type=gha,mode=max
+          # cache-from: type=gha
+          # cache-to: type=gha,mode=max
           tags: ${{ steps.meta.outputs.tags }}