Compare commits

...

18 commits

Author SHA1 Message Date
1a1e658fa9 [fix] ci
Some checks failed
mkdocs / mkdocs (push) Failing after 56s
docker / build-docker (linux/386) (push) Failing after 54s
docker / build-docker (linux/arm/v5) (push) Failing after 25s
docker / build-docker (linux/amd64) (push) Failing after 18s
docker / build-docker (linux/arm/v7) (push) Failing after 16s
docker / build-docker (linux/arm64) (push) Failing after 12s
docker / build-docker (linux/arm64/v8) (push) Failing after 22s
docker / build-docker (linux/riscv64) (push) Failing after 20s
2024-03-28 16:18:28 +03:00
ef751771d2 update 2024-03-28 16:09:42 +03:00
7b1863109c context fs 2024-03-21 00:47:51 +03:00
fd3beea874 fs is dir entry 2024-03-20 13:49:19 +03:00
e576e62599 gql dir ls 2024-03-20 00:30:37 +03:00
6a1e338af4 update 2024-03-18 00:00:34 +03:00
35913e0190 WIP 2024-02-23 01:54:56 +03:00
b97dcc8d8f wip 2024-01-28 23:22:49 +03:00
2cefb9db98 WIP 2024-01-07 20:09:56 +03:00
78704bee78 web 2024-01-01 21:17:40 +03:00
7d9f1a437c qol 2024-01-01 21:17:32 +03:00
49444bd70d file delete on exclude 2024-01-01 01:54:55 +03:00
5f8d497de1 rename 2023-12-27 00:14:36 +03:00
cd6cf8dd74 rework exclude repository 2023-12-26 22:30:19 +03:00
0332206560 [feature] file exclude 2023-12-26 01:11:03 +03:00
0350ecba9a oprimized, working 2023-12-22 02:15:39 +03:00
2b39afca3b update readme 2023-10-18 14:07:19 +03:00
ec83e3b08b docker deploy 2023-10-18 12:52:48 +03:00
168 changed files with 22253 additions and 1818 deletions

12
.github/FUNDING.yml vendored
View file

@ -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']

View file

@ -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

View file

@ -9,6 +9,9 @@ on:
jobs:
build-docker:
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
@ -16,24 +19,22 @@ 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
- 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
registry: git.kmsign.ru
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
@ -42,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
@ -54,7 +55,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
@ -63,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 }}

4
.gitignore vendored
View file

@ -2,4 +2,6 @@ tstor-data
httpfs_vfsdata.go
bin/
coverage.out
bin
bin
build
deploy-debug.sh

52
.golangci.yml Normal file
View file

@ -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

48
.gqlgen.yml Normal file
View file

@ -0,0 +1,48 @@
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"
# TorrentProgress:
# fields:
# torrent:
# resolver: true

4
.graphqlrc.yaml Normal file
View file

@ -0,0 +1,4 @@
schema:
- graphql/schema.graphql
- graphql/*.graphql
- graphql/**/*.graphql

9
.vscode/launch.json vendored
View file

@ -4,13 +4,20 @@
// 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",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/cmd/tstor/main.go",
"cwd": "${workspaceFolder}/bin",
"cwd": "${workspaceFolder}/bin"
}
]
}

View file

@ -1,5 +0,0 @@
{
"yaml.schemas": {
"https://json.schemastore.org/github-workflow.json": "file:///home/royalcat/projects/distribyted/.github/workflows/mkdocs.yml"
}
}

View file

@ -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"]

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 fieldDirectiveFix struct {
}
func (fieldDirectiveFix) Name() string {
return "Fix Directive hook called with wrong object"
}
func (fieldDirectiveFix) 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(&fieldDirectiveFix{}),
)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(3)
}
}

View file

@ -1,33 +1,39 @@
package main
import (
"bufio"
"context"
"fmt"
"net"
nethttp "net/http"
_ "net/http/pprof"
"os"
"os/signal"
"path/filepath"
"runtime"
"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"
"git.kmsign.ru/royalcat/tstor/src/host/torrent"
"github.com/anacrolix/torrent/storage"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"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"
"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/webdav"
_ "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"
)
const (
configFlag = "config"
fuseAllowOther = "fuse-allow-other"
portFlag = "http-port"
webDAVPortFlag = "webdav-port"
)
@ -45,179 +51,210 @@ func main() {
},
Action: func(c *cli.Context) error {
err := load(c.String(configFlag), c.Int(portFlag), c.Int(webDAVPortFlag), c.Bool(fuseAllowOther))
// 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,
}
if err := app.Run(os.Args); err != nil {
log.Fatal().Err(err).Msg("problem starting application")
print("problem starting application: ", err.Error())
}
}
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)
}
// 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, port, webDAVPort int, fuseAllowOther bool) 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)
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 := rlog.ComponentLog("run")
// TODO make optional
err = syscall.Setpriority(syscall.PRIO_PGRP, 0, 19)
if err != nil {
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 := torrent.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 := torrent.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 := setupStorage(conf.TorrentClient)
st, _, err := datastorage.Setup(conf.TorrentClient)
if err != nil {
return err
}
defer st.Close()
excludedFilesStore, err := store.NewFileMappings(conf.TorrentClient.MetadataFolder, st)
if err != nil {
return err
}
c, err := torrent.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 := torrent.NewService(c, 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)
}
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)
sfs = vfs.WrapLogFS(sfs)
// 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)
// }
// }
// }
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(sfs)
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 {
log.Error().Err(err).Msg("error starting webDAV")
if err := webdav.NewWebDAVServer(sfs, conf.Mounts.WebDAV.Port, conf.Mounts.WebDAV.User, conf.Mounts.WebDAV.Pass); err != nil {
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 {
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("error starting HTTPFS", "error", err)
}
// r := gin.New()
r := gin.New()
// r.GET("*filepath", func(c *gin.Context) {
// path := c.Param("filepath")
// c.FileFromFS(path, httpfs)
// })
r.GET("*filepath", func(c *gin.Context) {
path := c.Param("filepath")
c.FileFromFS(path, 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")
// }
}()
}
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")
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))
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)
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
}
}()
}
logFilename := filepath.Join(conf.Log.Path, dlog.FileName)
go func() {
err := webdav.NewDirServer(conf.SourceDir, 36912, conf.Mounts.WebDAV.User, conf.Mounts.WebDAV.Pass)
if err != nil {
log.Error("error starting webDAV", "error", err)
}
}()
err = http.New(nil, nil, ts, logFilename, conf)
log.Error().Err(err).Msg("error initializing HTTP server")
return err
go func() {
logFilename := filepath.Join(conf.Log.Path, "logs")
err := delivery.New(nil, service.NewStats(), ts, sfs, logFilename, conf)
if err != nil {
log.Error("error initializing HTTP server", "error", err)
}
}()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
return ts.Close()
}

117
go.mod
View file

@ -1,61 +1,90 @@
module git.kmsign.ru/royalcat/tstor
go 1.21
go 1.22.1
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/99designs/gqlgen v0.17.43
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
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/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
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
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/nwaples/rardecode/v2 v2.0.0-beta.2
github.com/rs/zerolog v1.31.0
github.com/rasky/go-xdr v0.0.0-20170124162913-1a41d1a06c93
github.com/ravilushqa/otelgqlgen v0.15.0
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
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
gopkg.in/natefinch/lumberjack.v2 v2.2.1
github.com/urfave/cli/v2 v2.27.0
github.com/vektah/gqlparser/v2 v2.5.11
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
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
golang.org/x/sys v0.17.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
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/sync v0.5.1 // 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/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
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/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/fatih/structs v1.1.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
@ -63,30 +92,31 @@ 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.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.0.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.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.3.0 // 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/hashicorp/go-multierror v1.1.1 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.16.6 // indirect
github.com/klauspost/compress v1.17.4 // 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.19 // 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
@ -94,7 +124,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.0.8 // indirect
github.com/pierrec/lz4/v4 v4.1.18 // 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
github.com/pion/ice/v2 v2.2.6 // indirect
@ -115,28 +145,43 @@ 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/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/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
github.com/ulikunitz/xz v0.5.11 // 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.22.5 // indirect
go.opentelemetry.io/otel v1.8.0 // indirect
go.opentelemetry.io/otel/trace v1.8.0 // indirect
go.opencensus.io v0.24.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/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.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
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/text v0.14.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.16.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

291
go.sum
View file

@ -19,8 +19,12 @@ 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/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=
@ -28,6 +32,12 @@ 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/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=
@ -42,23 +52,23 @@ 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.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=
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=
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.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=
@ -72,8 +82,8 @@ github.com/anacrolix/missinggo/perf v1.0.0/go.mod h1:ljAFWkBuzkO12MQclXzZrosP5ur
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=
@ -84,20 +94,24 @@ github.com/anacrolix/stm v0.4.1-0.20221221005312-96d17df0e496 h1:aMiRi2kOOd+nG64
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.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/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=
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/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/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=
@ -105,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=
@ -113,8 +128,8 @@ github.com/bits-and-blooms/bitset v1.2.2 h1:J5gbX05GpMdBjCvQ9MteIg2KKDExr7DrgK+Y
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.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=
@ -124,7 +139,11 @@ 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=
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=
@ -135,10 +154,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/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=
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=
@ -148,6 +170,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=
@ -157,7 +181,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=
@ -170,8 +196,11 @@ github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4
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=
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 +209,9 @@ 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=
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,30 +223,37 @@ 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.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=
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.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
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/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=
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.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=
@ -232,52 +271,64 @@ 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=
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.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=
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=
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=
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=
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=
@ -295,12 +346,13 @@ 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=
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=
@ -320,6 +372,7 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
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,14 +380,19 @@ 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/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=
@ -352,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=
@ -365,13 +424,16 @@ 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=
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/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/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.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=
@ -422,23 +484,44 @@ 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=
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=
@ -446,27 +529,46 @@ 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/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-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=
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/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=
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=
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=
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=
@ -492,14 +594,26 @@ github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDW
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 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.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.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.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-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=
@ -513,12 +627,32 @@ 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=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
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=
go4.org v0.0.0-20200411211856-f5505b9728dd/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZMZXMSBGhxRdsvzbkg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
@ -530,13 +664,14 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
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.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=
@ -545,8 +680,8 @@ 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-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=
@ -566,6 +701,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=
@ -587,6 +724,7 @@ golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLL
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=
@ -601,8 +739,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
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.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.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=
@ -616,8 +754,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=
@ -641,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=
@ -649,6 +788,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
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-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-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=
@ -663,8 +804,8 @@ 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.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.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=
@ -680,12 +821,12 @@ 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.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=
@ -693,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=
@ -715,8 +857,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
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.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,41 +890,58 @@ 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-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=
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.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=
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.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=
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-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=
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=
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=

25
graphql/mutation.graphql Normal file
View file

@ -0,0 +1,25 @@
type Mutation {
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!
}
type DownloadTorrentResponse {
task: Task
}
type CleanupResponse {
count: Int!
list: [String!]!
}
type Task {
id: ID!
}

49
graphql/query.graphql Normal file
View file

@ -0,0 +1,49 @@
type Query {
torrents(filter: TorrentsFilter, pagination: Pagination): [Torrent!]!
fsListDir(path: String!): ListDirResponse!
}
input TorrentsFilter {
name: StringFilter
bytesCompleted: IntFilter
bytesMissing: IntFilter
peersCount: IntFilter
}
type ListDirResponse {
root: DirEntry!
entries: [DirEntry!]!
}
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
}

9
graphql/schema.graphql Normal file
View file

@ -0,0 +1,9 @@
directive @oneOf on INPUT_OBJECT | FIELD_DEFINITION
directive @stream on FIELD_DEFINITION
scalar DateTime
type Schema {
query: Query
mutation: Mutation
}

View file

@ -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!
}

26
graphql/types/fs.graphql Normal file
View file

@ -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!
}

View file

@ -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!
}

27
pkg/ctxbilly/change.go Normal file
View file

@ -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
}

92
pkg/ctxbilly/fs.go Normal file
View file

@ -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
}

166
pkg/ctxbilly/mem.go Normal file
View file

@ -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)
}

63
pkg/ctxio/cachereader.go Normal file
View file

@ -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)
}

89
pkg/ctxio/copy.go Normal file
View file

@ -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
// }

180
pkg/ctxio/filebuffer.go Normal file
View file

@ -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
}

663
pkg/ctxio/io.go Normal file
View file

@ -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)]
}
}
}

64
pkg/ctxio/reader.go Normal file
View file

@ -0,0 +1,64 @@
package ctxio
import (
"context"
"io"
)
type FileReader interface {
Reader
ReaderAt
Closer
}
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)
}
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) {
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)
}

102
pkg/ctxio/seeker.go Normal file
View file

@ -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 FileReader
}
func IoReadSeekCloserWrapper(ctx context.Context, r FileReader, 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)
}

20
pkg/ctxio/teereader.go Normal file
View file

@ -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
// }

11
pkg/go-nfs/.github/dependabot.yml vendored Normal file
View file

@ -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"

View file

@ -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

36
pkg/go-nfs/.github/workflows/go.yml vendored Normal file
View file

@ -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 .

View file

@ -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.

202
pkg/go-nfs/LICENSE Normal file
View file

@ -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.

96
pkg/go-nfs/README.md Normal file
View file

@ -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)

11
pkg/go-nfs/SECURITY.md Normal file
View file

@ -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.

View file

@ -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
}

335
pkg/go-nfs/conn.go Normal file
View file

@ -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
}

230
pkg/go-nfs/errors.go Normal file
View file

@ -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[:])
)

View file

@ -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))
}

View file

@ -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)
}

View file

@ -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)})
}

View file

@ -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))
}

View file

@ -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))
}

377
pkg/go-nfs/file.go Normal file
View file

@ -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
}

17
pkg/go-nfs/file/file.go Normal file
View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

101
pkg/go-nfs/filesystem.go Normal file
View file

@ -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
}

52
pkg/go-nfs/handler.go Normal file
View file

@ -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
}

View file

@ -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)
}

View file

@ -0,0 +1,198 @@
package helpers
import (
"crypto/sha256"
"encoding/binary"
"io/fs"
"reflect"
"git.kmsign.ru/royalcat/tstor/pkg/go-nfs"
"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 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 nfs.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) (nfs.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)
}
}
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 nfs.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 nfs.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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

216
pkg/go-nfs/log.go Normal file
View file

@ -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...)
}

58
pkg/go-nfs/mount.go Normal file
View file

@ -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)
}

View file

@ -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
}

38
pkg/go-nfs/nfs.go Normal file
View file

@ -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{})
}

View file

@ -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
}

View file

@ -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
}

125
pkg/go-nfs/nfs_oncreate.go Normal file
View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

94
pkg/go-nfs/nfs_onlink.go Normal file
View file

@ -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
}

View file

@ -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
}

94
pkg/go-nfs/nfs_onmkdir.go Normal file
View file

@ -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
}

158
pkg/go-nfs/nfs_onmknod.go Normal file
View file

@ -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
}

View file

@ -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
}

97
pkg/go-nfs/nfs_onread.go Normal file
View file

@ -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
}

195
pkg/go-nfs/nfs_onreaddir.go Normal file
View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

120
pkg/go-nfs/nfs_onrename.go Normal file
View file

@ -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
}

View file

@ -0,0 +1,9 @@
package nfs
import (
"context"
)
func onRmDir(ctx context.Context, w *response, userHandle Handler) error {
return onRemove(ctx, w, userHandle)
}

View file

@ -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
}

View file

@ -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
}

116
pkg/go-nfs/nfs_onwrite.go Normal file
View file

@ -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
}

293
pkg/go-nfs/nfs_test.go Normal file
View file

@ -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
}

188
pkg/go-nfs/nfsinterface.go Normal file
View file

@ -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
}

102
pkg/go-nfs/server.go Normal file
View file

@ -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)
}

32
pkg/go-nfs/time.go Normal file
View file

@ -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)
}

90
pkg/kvtrace/kvmetrics.go Normal file
View file

@ -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)

70
pkg/rlog/rlog.go Normal file
View file

@ -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...)
}

102
pkg/uuid/uuid.go Normal file
View file

@ -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)
}
}

View file

@ -1,7 +1,7 @@
package config
var defaultConfig = Config{
DataFolder: "./data",
var defaultConfig = Settings{
SourceDir: "./data",
WebUi: WebUi{
Port: 4444,
IP: "0.0.0.0",
@ -20,6 +20,11 @@ var defaultConfig = Config{
Fuse: Fuse{
Enabled: false,
},
NFS: NFS{
Enabled: false,
Port: 8122,
CachePath: "./nfs-cache",
},
},
TorrentClient: TorrentClient{
@ -29,8 +34,8 @@ var defaultConfig = Config{
// GlobalCacheSize: 2048,
AddTimeout: 60,
ReadTimeout: 120,
// AddTimeout: 60,
// ReadTimeout: 120,
},
Log: Log{

View file

@ -13,15 +13,25 @@ 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
}
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 {
@ -41,8 +51,11 @@ func Load(path string) (*Config, error) {
return nil, err
}
conf := Config{}
k.Unmarshal("", &conf)
conf := Settings{}
err = k.Unmarshal("", &conf)
if err != nil {
return nil, err
}
return &conf, nil
}

View file

@ -1,13 +1,15 @@
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"`
Log Log `koanf:"log"`
DataFolder string `koanf:"dataFolder"`
SourceDir string `koanf:"source_dir"`
OtelHttp string `koanf:"otel_http"`
}
type WebUi struct {
@ -24,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"`
@ -61,6 +63,13 @@ 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"`
CachePath string `koanf:"cache_path"`
}
type HttpFs struct {

View file

@ -1,4 +1,4 @@
package http
package delivery
import (
"bytes"
@ -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(),
@ -29,7 +29,7 @@ var apiStatusHandler = func(fc *filecache.Cache, ss *torrent.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 *torrent.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 *torrent.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 *torrent.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")

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,21 @@
package model
import "slices"
func (f *IntFilter) Include(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
}

View file

@ -0,0 +1,37 @@
package model
import (
"git.kmsign.ru/royalcat/tstor/src/host/controller"
"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"
}
}
func MapTorrent(t *controller.Torrent) *Torrent {
return &Torrent{
Infohash: t.InfoHash(),
Name: t.Name(),
BytesCompleted: t.BytesCompleted(),
BytesMissing: t.BytesMissing(),
T: t,
}
}

View file

@ -0,0 +1,173 @@
// 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 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"`
}
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"`
Lt *time.Time `json:"lt,omitempty"`
Gte *time.Time `json:"gte,omitempty"`
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"`
Lt *int64 `json:"lt,omitempty"`
Gte *int64 `json:"gte,omitempty"`
Lte *int64 `json:"lte,omitempty"`
In []int64 `json:"in,omitempty"`
}
type ListDirResponse struct {
Root DirEntry `json:"root"`
Entries []DirEntry `json:"entries"`
}
type Mutation struct {
}
type Pagination struct {
Offset int64 `json:"offset"`
Limit int64 `json:"limit"`
}
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"`
}
type StringFilter struct {
Eq *string `json:"eq,omitempty"`
Substr *string `json:"substr,omitempty"`
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"`
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 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"`
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 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"`
BytesMissing *IntFilter `json:"bytesMissing,omitempty"`
PeersCount *IntFilter `json:"peersCount,omitempty"`
}

Some files were not shown because too many files have changed in this diff Show more