Compare commits
18 commits
f8de016460
...
1a1e658fa9
Author | SHA1 | Date | |
---|---|---|---|
1a1e658fa9 | |||
ef751771d2 | |||
7b1863109c | |||
fd3beea874 | |||
e576e62599 | |||
6a1e338af4 | |||
35913e0190 | |||
b97dcc8d8f | |||
2cefb9db98 | |||
78704bee78 | |||
7d9f1a437c | |||
49444bd70d | |||
5f8d497de1 | |||
cd6cf8dd74 | |||
0332206560 | |||
0350ecba9a | |||
2b39afca3b | |||
ec83e3b08b |
168 changed files with 22253 additions and 1818 deletions
12
.github/FUNDING.yml
vendored
12
.github/FUNDING.yml
vendored
|
@ -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']
|
59
.github/workflows/codeql-analysis.yml
vendored
59
.github/workflows/codeql-analysis.yml
vendored
|
@ -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
|
27
.github/workflows/docker.yaml
vendored
27
.github/workflows/docker.yaml
vendored
|
@ -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
4
.gitignore
vendored
|
@ -2,4 +2,6 @@ tstor-data
|
|||
httpfs_vfsdata.go
|
||||
bin/
|
||||
coverage.out
|
||||
bin
|
||||
bin
|
||||
build
|
||||
deploy-debug.sh
|
52
.golangci.yml
Normal file
52
.golangci.yml
Normal 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
48
.gqlgen.yml
Normal 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
4
.graphqlrc.yaml
Normal file
|
@ -0,0 +1,4 @@
|
|||
schema:
|
||||
- graphql/schema.graphql
|
||||
- graphql/*.graphql
|
||||
- graphql/**/*.graphql
|
9
.vscode/launch.json
vendored
9
.vscode/launch.json
vendored
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"yaml.schemas": {
|
||||
"https://json.schemastore.org/github-workflow.json": "file:///home/royalcat/projects/distribyted/.github/workflows/mkdocs.yml"
|
||||
}
|
||||
}
|
41
Dockerfile
41
Dockerfile
|
@ -1,33 +1,24 @@
|
|||
#===============
|
||||
# Stage 1: Build
|
||||
#===============
|
||||
FROM golang:1.21 as builder
|
||||
|
||||
FROM golang:1.20 as builder
|
||||
WORKDIR /app
|
||||
|
||||
ENV BIN_REPO=git.kmsign.ru/royalcat/tstor
|
||||
ENV BIN_PATH=$GOPATH/src/$BIN_REPO
|
||||
COPY go.mod ./
|
||||
COPY go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . $BIN_PATH
|
||||
WORKDIR $BIN_PATH
|
||||
COPY ./src ./src
|
||||
COPY ./cmd ./cmd
|
||||
COPY ./assets ./assets
|
||||
COPY ./templates ./templates
|
||||
COPY embed.go embed.go
|
||||
|
||||
RUN apk add fuse-dev git gcc libc-dev g++ make
|
||||
RUN go generate ./...
|
||||
RUN CGO_ENABLED=0 go build -tags timetzdata -o /tstor ./cmd/tstor/main.go
|
||||
|
||||
RUN BIN_OUTPUT=/bin/tstor make build
|
||||
|
||||
#===============
|
||||
# Stage 2: Run
|
||||
#===============
|
||||
FROM scratch
|
||||
|
||||
FROM alpine:3
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
COPY --from=builder /tstor /tstor
|
||||
|
||||
RUN apk add gcc libc-dev fuse-dev
|
||||
|
||||
COPY --from=builder /bin/tstor /bin/tstor
|
||||
RUN chmod +x /bin/tstor
|
||||
|
||||
RUN mkdir /tstor-data
|
||||
|
||||
RUN echo "user_allow_other" >> /etc/fuse.conf
|
||||
ENV tstor_FUSE_ALLOW_OTHER=true
|
||||
|
||||
ENTRYPOINT ["./bin/tstor"]
|
||||
ENTRYPOINT ["/tstor"]
|
||||
|
|
12
Makefile
12
Makefile
|
@ -34,11 +34,7 @@ go-generate:
|
|||
@echo " > Generating code files..."
|
||||
go generate ./...
|
||||
|
||||
.PHONY: help
|
||||
all: help
|
||||
help: Makefile
|
||||
@echo
|
||||
@echo " Choose a command run in "$(PROJECTNAME)":"
|
||||
@echo
|
||||
@sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /'
|
||||
@echo
|
||||
generate-graphql: src/delivery/graph/generated.go
|
||||
|
||||
src/delivery/graph/generated.go: .gqlgen.yml graphql/* graphql/types/* cmd/generate-graphql/*
|
||||
go run cmd/generate-graphql/main.go
|
||||
|
|
91
README.md
91
README.md
|
@ -1,93 +1,18 @@
|
|||
[![Releases][releases-shield]][releases-url]
|
||||
[![Contributors][contributors-shield]][contributors-url]
|
||||
[![Forks][forks-shield]][forks-url]
|
||||
[![Stargazers][stars-shield]][stars-url]
|
||||
[![Issues][issues-shield]][issues-url]
|
||||
[![GPL3 License][license-shield]][license-url]
|
||||
[![Coveralls][coveralls-shield]][coveralls-url]
|
||||
[![Docker Image][docker-pulls-shield]][docker-pulls-url]
|
||||
# tstor (WIP)
|
||||
|
||||
<!-- PROJECT LOGO -->
|
||||
<br />
|
||||
<p align="center">
|
||||
<a href="https://git.kmsign.ru/royalcat/tstor">
|
||||
<img src="mkdocs/docs/images/tstor_icon.png" alt="Logo" width="100">
|
||||
</a>
|
||||
tstor is an advanced remote torrent clien for self-hosting enthusiasts.
|
||||
|
||||
<h3 align="center">tstor</h3>
|
||||
It expose virtual filesystem with torrents and archives presented as fully featured directories with limited amount of mutability. Virtual filesystem can be exported as a webDAV, HTTP endpoint or NFS(WIP).
|
||||
|
||||
<p align="center">
|
||||
Torrent client with on-demand file downloading as a filesystem.
|
||||
<br />
|
||||
<br />
|
||||
<a href="https://git.kmsign.ru/royalcat/tstor/issues">Report a Bug</a>
|
||||
·
|
||||
<a href="https://git.kmsign.ru/royalcat/tstor/issues">Request Feature</a>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
## About The Project
|
||||
|
||||
![tstor Screen Shot][product-screenshot]
|
||||
|
||||
tstor is an alternative torrent client.
|
||||
It can expose torrent files as a standard FUSE, webDAV or HTTP endpoint and download them on demand, allowing random reads using a fixed amount of disk space.
|
||||
|
||||
tstor tries to make easier integrations with other applications using torrent files, presenting them as a standard filesystem.
|
||||
|
||||
**Note that tstor is in beta version, it is a proof of concept with a lot of bugs.**
|
||||
|
||||
## Use Cases
|
||||
|
||||
- Play **multimedia files** on your favorite video or audio player. These files will be downloaded on demand and only the needed parts.
|
||||
- Explore TBs of data from public **datasets** only downloading the parts you need. Use **Jupyter Notebooks** directly to process or analyze this data.
|
||||
- Give access to your latest dataset creation just by sharing a magnet link. People will start using your data in seconds.
|
||||
- Play your **ROM backups** directly from the torrent file. You can have virtually GBs in games and only downloaded the needed ones.
|
||||
|
||||
## Documentation
|
||||
|
||||
Check [here][main-url] for further documentation.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
|
||||
|
||||
Some areas need more care than others:
|
||||
|
||||
- Windows and macOS tests and compatibility. I don't have any easy way to test tstor on these operating systems.
|
||||
- Web interface. Web development is not my _forte_.
|
||||
- Tutorials. Share with the community your use case!
|
||||
tstor is based on amazing [distribyted](https://github.com/distribyted/distribyted), but has more focus on store a torrent data when streaming it.
|
||||
|
||||
## Special thanks
|
||||
|
||||
- [Anacrolix BitTorrent client package and utilities][torrent-repo-url]. An amazing torrent library with file seek support.
|
||||
- [Nwaples rardecode library, experimental branch][rardecode-repo-url]. The only go library that is able to seek over rar files and avoid to use `io.Discard`.
|
||||
- [Bodgit 7zip library][sevenzip-repo-url]. Amazing library to decode 7zip files.
|
||||
- [distribyted](https://github.com/distribyted/distribyted)
|
||||
- [Anacrolix BitTorrent client package and utilities](https://github.com/anacrolix/torrent-repo-url). An amazing torrent library with file seek support.
|
||||
- [Nwaples rardecode library, experimental branch](https://github.com/nwaples/rardecode/tree/experimental). The only go library that is able to seek over rar files and avoid to use `io.Discard`.
|
||||
- [Bodgit 7zip library](https://github.com/bodgit/sevenzip). Amazing library to decode 7zip files.
|
||||
|
||||
## License
|
||||
|
||||
Distributed under the GPL3 license. See `LICENSE` for more information.
|
||||
|
||||
[sevenzip-repo-url]: https://github.com/bodgit/sevenzip
|
||||
[rardecode-repo-url]: https://github.com/nwaples/rardecode/tree/experimental
|
||||
[torrent-repo-url]: https://github.com/anacrolix/torrent
|
||||
[main-url]: https://tstor.com
|
||||
[releases-shield]: https://img.shields.io/github/v/release/tstor/tstor.svg?style=flat-square
|
||||
[releases-url]: https://git.kmsign.ru/royalcat/tstor/releases
|
||||
[docker-pulls-shield]: https://img.shields.io/docker/pulls/tstor/tstor.svg?style=flat-square
|
||||
[docker-pulls-url]: https://hub.docker.com/r/tstor/tstor
|
||||
[contributors-shield]: https://img.shields.io/github/contributors/tstor/tstor.svg?style=flat-square
|
||||
[contributors-url]: https://git.kmsign.ru/royalcat/tstor/graphs/contributors
|
||||
[forks-shield]: https://img.shields.io/github/forks/tstor/tstor.svg?style=flat-square
|
||||
[forks-url]: https://git.kmsign.ru/royalcat/tstor/network/members
|
||||
[stars-shield]: https://img.shields.io/github/stars/tstor/tstor.svg?style=flat-square
|
||||
[stars-url]: https://git.kmsign.ru/royalcat/tstor/stargazers
|
||||
[issues-shield]: https://img.shields.io/github/issues/tstor/tstor.svg?style=flat-square
|
||||
[issues-url]: https://git.kmsign.ru/royalcat/tstor/issues
|
||||
[releases-url]: https://git.kmsign.ru/royalcat/tstor/releases
|
||||
[license-shield]: https://img.shields.io/github/license/tstor/tstor.svg?style=flat-square
|
||||
[license-url]: https://git.kmsign.ru/royalcat/tstor/blob/master/LICENSE
|
||||
[product-screenshot]: mkdocs/docs/images/tstor.gif
|
||||
[example-config]: https://git.kmsign.ru/royalcat/tstor/blob/master/examples/conf_example.yaml
|
||||
[coveralls-shield]: https://img.shields.io/coveralls/github/tstor/tstor?style=flat-square
|
||||
[coveralls-url]: https://coveralls.io/github/tstor/tstor
|
||||
|
|
|
@ -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
|
65
cmd/generate-graphql/main.go
Normal file
65
cmd/generate-graphql/main.go
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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
117
go.mod
|
@ -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
291
go.sum
|
@ -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
25
graphql/mutation.graphql
Normal 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
49
graphql/query.graphql
Normal 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
9
graphql/schema.graphql
Normal 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
|
||||
}
|
16
graphql/subscription.graphql
Normal file
16
graphql/subscription.graphql
Normal 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
26
graphql/types/fs.graphql
Normal 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!
|
||||
}
|
24
graphql/types/torrent.graphql
Normal file
24
graphql/types/torrent.graphql
Normal 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
27
pkg/ctxbilly/change.go
Normal 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
92
pkg/ctxbilly/fs.go
Normal 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
166
pkg/ctxbilly/mem.go
Normal 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
63
pkg/ctxio/cachereader.go
Normal 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
89
pkg/ctxio/copy.go
Normal 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
180
pkg/ctxio/filebuffer.go
Normal 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
663
pkg/ctxio/io.go
Normal 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
64
pkg/ctxio/reader.go
Normal 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
102
pkg/ctxio/seeker.go
Normal 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
20
pkg/ctxio/teereader.go
Normal 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
11
pkg/go-nfs/.github/dependabot.yml
vendored
Normal 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"
|
51
pkg/go-nfs/.github/workflows/codeql-analysis.yml
vendored
Normal file
51
pkg/go-nfs/.github/workflows/codeql-analysis.yml
vendored
Normal 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
36
pkg/go-nfs/.github/workflows/go.yml
vendored
Normal 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 .
|
11
pkg/go-nfs/CONTRIBUTING.md
Normal file
11
pkg/go-nfs/CONTRIBUTING.md
Normal 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
202
pkg/go-nfs/LICENSE
Normal 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
96
pkg/go-nfs/README.md
Normal 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
11
pkg/go-nfs/SECURITY.md
Normal 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.
|
9
pkg/go-nfs/capability_check.go
Normal file
9
pkg/go-nfs/capability_check.go
Normal 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
335
pkg/go-nfs/conn.go
Normal 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
230
pkg/go-nfs/errors.go
Normal 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[:])
|
||||
)
|
52
pkg/go-nfs/example/helloworld/main.go
Normal file
52
pkg/go-nfs/example/helloworld/main.go
Normal 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))
|
||||
}
|
38
pkg/go-nfs/example/osnfs/changeos.go
Normal file
38
pkg/go-nfs/example/osnfs/changeos.go
Normal 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)
|
||||
}
|
28
pkg/go-nfs/example/osnfs/changeos_unix.go
Normal file
28
pkg/go-nfs/example/osnfs/changeos_unix.go
Normal 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)})
|
||||
}
|
36
pkg/go-nfs/example/osnfs/main.go
Normal file
36
pkg/go-nfs/example/osnfs/main.go
Normal 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))
|
||||
}
|
37
pkg/go-nfs/example/osview/main.go
Normal file
37
pkg/go-nfs/example/osview/main.go
Normal 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
377
pkg/go-nfs/file.go
Normal 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
17
pkg/go-nfs/file/file.go
Normal 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)
|
||||
}
|
24
pkg/go-nfs/file/file_unix.go
Normal file
24
pkg/go-nfs/file/file_unix.go
Normal 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
|
||||
}
|
12
pkg/go-nfs/file/file_windows.go
Normal file
12
pkg/go-nfs/file/file_windows.go
Normal 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
101
pkg/go-nfs/filesystem.go
Normal 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
52
pkg/go-nfs/handler.go
Normal 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
|
||||
}
|
157
pkg/go-nfs/helpers/billlyfs.go
Normal file
157
pkg/go-nfs/helpers/billlyfs.go
Normal 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)
|
||||
}
|
198
pkg/go-nfs/helpers/cachinghandler.go
Normal file
198
pkg/go-nfs/helpers/cachinghandler.go
Normal 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
|
||||
}
|
414
pkg/go-nfs/helpers/memfs/memfs.go
Normal file
414
pkg/go-nfs/helpers/memfs/memfs.go
Normal 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
|
||||
}
|
243
pkg/go-nfs/helpers/memfs/storage.go
Normal file
243
pkg/go-nfs/helpers/memfs/storage.go
Normal 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
|
||||
}
|
59
pkg/go-nfs/helpers/nullauthhandler.go
Normal file
59
pkg/go-nfs/helpers/nullauthhandler.go
Normal 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
216
pkg/go-nfs/log.go
Normal 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
58
pkg/go-nfs/mount.go
Normal 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)
|
||||
}
|
90
pkg/go-nfs/mountinterface.go
Normal file
90
pkg/go-nfs/mountinterface.go
Normal 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
38
pkg/go-nfs/nfs.go
Normal 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{})
|
||||
}
|
45
pkg/go-nfs/nfs_onaccess.go
Normal file
45
pkg/go-nfs/nfs_onaccess.go
Normal 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
|
||||
}
|
51
pkg/go-nfs/nfs_oncommit.go
Normal file
51
pkg/go-nfs/nfs_oncommit.go
Normal 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
125
pkg/go-nfs/nfs_oncreate.go
Normal 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
|
||||
}
|
89
pkg/go-nfs/nfs_onfsinfo.go
Normal file
89
pkg/go-nfs/nfs_onfsinfo.go
Normal 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
|
||||
}
|
59
pkg/go-nfs/nfs_onfsstat.go
Normal file
59
pkg/go-nfs/nfs_onfsstat.go
Normal 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
|
||||
}
|
48
pkg/go-nfs/nfs_ongetattr.go
Normal file
48
pkg/go-nfs/nfs_ongetattr.go
Normal 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
94
pkg/go-nfs/nfs_onlink.go
Normal 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
|
||||
}
|
86
pkg/go-nfs/nfs_onlookup.go
Normal file
86
pkg/go-nfs/nfs_onlookup.go
Normal 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
94
pkg/go-nfs/nfs_onmkdir.go
Normal 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
158
pkg/go-nfs/nfs_onmknod.go
Normal 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
|
||||
}
|
55
pkg/go-nfs/nfs_onpathconf.go
Normal file
55
pkg/go-nfs/nfs_onpathconf.go
Normal 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
97
pkg/go-nfs/nfs_onread.go
Normal 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
195
pkg/go-nfs/nfs_onreaddir.go
Normal 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)
|
||||
}
|
153
pkg/go-nfs/nfs_onreaddirplus.go
Normal file
153
pkg/go-nfs/nfs_onreaddirplus.go
Normal 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
|
||||
}
|
55
pkg/go-nfs/nfs_onreadlink.go
Normal file
55
pkg/go-nfs/nfs_onreadlink.go
Normal 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
|
||||
}
|
85
pkg/go-nfs/nfs_onremove.go
Normal file
85
pkg/go-nfs/nfs_onremove.go
Normal 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
120
pkg/go-nfs/nfs_onrename.go
Normal 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
|
||||
}
|
9
pkg/go-nfs/nfs_onrmdir.go
Normal file
9
pkg/go-nfs/nfs_onrmdir.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package nfs
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
func onRmDir(ctx context.Context, w *response, userHandle Handler) error {
|
||||
return onRemove(ctx, w, userHandle)
|
||||
}
|
80
pkg/go-nfs/nfs_onsetattr.go
Normal file
80
pkg/go-nfs/nfs_onsetattr.go
Normal 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
|
||||
}
|
88
pkg/go-nfs/nfs_onsymlink.go
Normal file
88
pkg/go-nfs/nfs_onsymlink.go
Normal 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
116
pkg/go-nfs/nfs_onwrite.go
Normal 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
293
pkg/go-nfs/nfs_test.go
Normal 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
188
pkg/go-nfs/nfsinterface.go
Normal 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
102
pkg/go-nfs/server.go
Normal 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
32
pkg/go-nfs/time.go
Normal 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
90
pkg/kvtrace/kvmetrics.go
Normal 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
70
pkg/rlog/rlog.go
Normal 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
102
pkg/uuid/uuid.go
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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{
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
7961
src/delivery/graphql/generated.go
Normal file
7961
src/delivery/graphql/generated.go
Normal file
File diff suppressed because it is too large
Load diff
21
src/delivery/graphql/model/filter.go
Normal file
21
src/delivery/graphql/model/filter.go
Normal 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
|
||||
}
|
37
src/delivery/graphql/model/mappers.go
Normal file
37
src/delivery/graphql/model/mappers.go
Normal 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,
|
||||
}
|
||||
}
|
173
src/delivery/graphql/model/models_gen.go
Normal file
173
src/delivery/graphql/model/models_gen.go
Normal 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
Loading…
Reference in a new issue