Compare commits
21 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b989cdabe5 | ||
![]() |
9e7e89d69e | ||
![]() |
f64056b820 | ||
![]() |
7e6d944cb5 | ||
![]() |
a66d207291 | ||
![]() |
0a8edcd811 | ||
![]() |
0fa08ddbaa | ||
![]() |
3d4c35522a | ||
![]() |
f400e67daa | ||
![]() |
4e10275fd1 | ||
![]() |
7bd71474ef | ||
![]() |
0ac2120532 | ||
![]() |
9e5287cfb4 | ||
![]() |
450ab6b252 | ||
![]() |
51d900558a | ||
![]() |
a71690ff2a | ||
![]() |
f390eab1de | ||
![]() |
571e088fdd | ||
![]() |
6714085d58 | ||
![]() |
0389605d65 | ||
![]() |
b8ef94ece7 |
85 changed files with 1882 additions and 738 deletions
2
.github/workflows/development.yml
vendored
2
.github/workflows/development.yml
vendored
|
@ -2,7 +2,7 @@ name: CI
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: [2.4.x]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
|
|
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
|
@ -5,7 +5,7 @@ on:
|
|||
# - cron: '0 4 * * *' # everyday at 4:00 AM UTC
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- 2.4.x
|
||||
tags:
|
||||
- v*
|
||||
pull_request:
|
||||
|
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
@ -5,7 +5,7 @@ on:
|
|||
tags: 'v*'
|
||||
|
||||
env:
|
||||
GO_VERSION: 1.19.2
|
||||
GO_VERSION: 1.19.3
|
||||
|
||||
jobs:
|
||||
prepare-sources-with-deps:
|
||||
|
|
1
CODEOWNERS
Normal file
1
CODEOWNERS
Normal file
|
@ -0,0 +1 @@
|
|||
* @drakkan
|
|
@ -28,6 +28,8 @@ ARG DOWNLOAD_PLUGINS=false
|
|||
|
||||
RUN if [ "${DOWNLOAD_PLUGINS}" = "true" ]; then apt-get update && apt-get install --no-install-recommends -y curl && ./docker/scripts/download-plugins.sh; fi
|
||||
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y openssh-server && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
# Set to "true" to install jq and the optional git and rsync dependencies
|
||||
|
@ -45,6 +47,7 @@ RUN groupadd --system -g 1000 sftpgo && \
|
|||
--comment "SFTPGo user" --uid 1000 sftpgo
|
||||
|
||||
COPY --from=builder /workspace/sftpgo.json /etc/sftpgo/sftpgo.json
|
||||
COPY --from=builder /etc/ssh/moduli /etc/sftpgo/moduli
|
||||
COPY --from=builder /workspace/templates /usr/share/sftpgo/templates
|
||||
COPY --from=builder /workspace/static /usr/share/sftpgo/static
|
||||
COPY --from=builder /workspace/openapi /usr/share/sftpgo/openapi
|
||||
|
|
|
@ -25,6 +25,7 @@ RUN set -xe && \
|
|||
export COMMIT_SHA=${COMMIT_SHA:-$(git describe --always --abbrev=8 --dirty)} && \
|
||||
go build $(if [ -n "${FEATURES}" ]; then echo "-tags ${FEATURES}"; fi) -trimpath -ldflags "-s -w -X github.com/drakkan/sftpgo/v2/internal/version.commit=${COMMIT_SHA} -X github.com/drakkan/sftpgo/v2/internal/version.date=`date -u +%FT%TZ`" -v -o sftpgo
|
||||
|
||||
RUN apk add --update --no-cache openssh-client-common
|
||||
|
||||
FROM alpine:3.16
|
||||
|
||||
|
@ -35,16 +36,13 @@ RUN apk add --update --no-cache ca-certificates tzdata mailcap
|
|||
|
||||
RUN if [ "${INSTALL_OPTIONAL_PACKAGES}" = "true" ]; then apk add --update --no-cache jq git rsync; fi
|
||||
|
||||
# set up nsswitch.conf for Go's "netgo" implementation
|
||||
# https://github.com/gliderlabs/docker-alpine/issues/367#issuecomment-424546457
|
||||
RUN test ! -e /etc/nsswitch.conf && echo 'hosts: files dns' > /etc/nsswitch.conf
|
||||
|
||||
RUN mkdir -p /etc/sftpgo /var/lib/sftpgo /usr/share/sftpgo /srv/sftpgo/data /srv/sftpgo/backups
|
||||
|
||||
RUN addgroup -g 1000 -S sftpgo && \
|
||||
adduser -u 1000 -h /var/lib/sftpgo -s /sbin/nologin -G sftpgo -S -D -H -g "SFTPGo user" sftpgo
|
||||
|
||||
COPY --from=builder /workspace/sftpgo.json /etc/sftpgo/sftpgo.json
|
||||
COPY --from=builder /etc/ssh/moduli /etc/sftpgo/moduli
|
||||
COPY --from=builder /workspace/templates /usr/share/sftpgo/templates
|
||||
COPY --from=builder /workspace/static /usr/share/sftpgo/static
|
||||
COPY --from=builder /workspace/openapi /usr/share/sftpgo/openapi
|
||||
|
|
|
@ -28,7 +28,7 @@ RUN sed -i 's|"users_base_dir": "",|"users_base_dir": "/srv/sftpgo/data",|' sftp
|
|||
sed -i 's|"backups"|"/srv/sftpgo/backups"|' sftpgo.json && \
|
||||
sed -i 's|"sqlite"|"bolt"|' sftpgo.json
|
||||
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y media-types && rm -rf /var/lib/apt/lists/*
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y media-types openssh-server && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir /etc/sftpgo /var/lib/sftpgo /srv/sftpgo
|
||||
|
||||
|
@ -38,6 +38,7 @@ COPY --from=builder --chown=1000:1000 /etc/sftpgo /etc/sftpgo
|
|||
COPY --from=builder --chown=1000:1000 /srv/sftpgo /srv/sftpgo
|
||||
COPY --from=builder --chown=1000:1000 /var/lib/sftpgo /var/lib/sftpgo
|
||||
COPY --from=builder --chown=1000:1000 /workspace/sftpgo.json /etc/sftpgo/sftpgo.json
|
||||
COPY --from=builder --chown=1000:1000 /etc/ssh/moduli /etc/sftpgo/moduli
|
||||
COPY --from=builder /workspace/templates /usr/share/sftpgo/templates
|
||||
COPY --from=builder /workspace/static /usr/share/sftpgo/static
|
||||
COPY --from=builder /workspace/openapi /usr/share/sftpgo/openapi
|
||||
|
|
|
@ -134,7 +134,7 @@ APT and YUM repositories are [available](./docs/repo.md).
|
|||
SFTPGo is also available on some marketplaces:
|
||||
|
||||
- [AWS Marketplace](https://aws.amazon.com/marketplace/seller-profile?id=6e849ab8-70a6-47de-9a43-13c3fa849335)
|
||||
- [Azure Marketplace](https://azuremarketplace.microsoft.com/en-us/marketplace/apps/prasselsrl1645470739547.sftpgo_linux)
|
||||
- [Azure Marketplace](https://azuremarketplace.microsoft.com/en-us/marketplace/apps/eliamarzia1667381463185.sftpgo_linux)
|
||||
- [Elest.io](https://elest.io/open-source/sftpgo)
|
||||
|
||||
Purchasing from there will help keep SFTPGo a long-term sustainable project.
|
||||
|
|
|
@ -125,7 +125,7 @@ SFTPGo 基于 Linux 开发和创建。在每一次提交之后,代码会自动
|
|||
|
||||
</details>
|
||||
|
||||
SFTPGo 在 [AWS Marketplace](https://aws.amazon.com/marketplace/seller-profile?id=6e849ab8-70a6-47de-9a43-13c3fa849335) 和 [Azure Marketplace](https://azuremarketplace.microsoft.com/en-us/marketplace/apps/prasselsrl1645470739547.sftpgo_linux) 同样可用,在此付费可以帮助 SFTPGo 成为一个可持续发展的长期项目。
|
||||
SFTPGo 在 [AWS Marketplace](https://aws.amazon.com/marketplace/seller-profile?id=6e849ab8-70a6-47de-9a43-13c3fa849335) 和 [Azure Marketplace](https://azuremarketplace.microsoft.com/en-us/marketplace/apps/eliamarzia1667381463185.sftpgo_linux) 同样可用,在此付费可以帮助 SFTPGo 成为一个可持续发展的长期项目。
|
||||
|
||||
<details><summary>Windows 包</summary>
|
||||
|
||||
|
|
|
@ -4,12 +4,12 @@ SFTPGo provides an official Docker image, it is available on both [Docker Hub](h
|
|||
|
||||
## Supported tags and respective Dockerfile links
|
||||
|
||||
- [v2.4.0, v2.4, v2, latest](https://github.com/drakkan/sftpgo/blob/v2.4.0/Dockerfile)
|
||||
- [v2.4.0-plugins, v2.4-plugins, v2-plugins, plugins](https://github.com/drakkan/sftpgo/blob/v2.4.0/Dockerfile)
|
||||
- [v2.4.0-alpine, v2.4-alpine, v2-alpine, alpine](https://github.com/drakkan/sftpgo/blob/v2.4.0/Dockerfile.alpine)
|
||||
- [v2.4.0-slim, v2.4-slim, v2-slim, slim](https://github.com/drakkan/sftpgo/blob/v2.4.0/Dockerfile)
|
||||
- [v2.4.0-alpine-slim, v2.4-alpine-slim, v2-alpine-slim, alpine-slim](https://github.com/drakkan/sftpgo/blob/v2.4.0/Dockerfile.alpine)
|
||||
- [v2.4.0-distroless-slim, v2.4-distroless-slim, v2-distroless-slim, distroless-slim](https://github.com/drakkan/sftpgo/blob/v2.4.0/Dockerfile.distroless)
|
||||
- [v2.4.2, v2.4, v2, latest](https://github.com/drakkan/sftpgo/blob/v2.4.2/Dockerfile)
|
||||
- [v2.4.2-plugins, v2.4-plugins, v2-plugins, plugins](https://github.com/drakkan/sftpgo/blob/v2.4.2/Dockerfile)
|
||||
- [v2.4.2-alpine, v2.4-alpine, v2-alpine, alpine](https://github.com/drakkan/sftpgo/blob/v2.4.2/Dockerfile.alpine)
|
||||
- [v2.4.2-slim, v2.4-slim, v2-slim, slim](https://github.com/drakkan/sftpgo/blob/v2.4.2/Dockerfile)
|
||||
- [v2.4.2-alpine-slim, v2.4-alpine-slim, v2-alpine-slim, alpine-slim](https://github.com/drakkan/sftpgo/blob/v2.4.2/Dockerfile.alpine)
|
||||
- [v2.4.2-distroless-slim, v2.4-distroless-slim, v2-distroless-slim, distroless-slim](https://github.com/drakkan/sftpgo/blob/v2.4.2/Dockerfile.distroless)
|
||||
- [edge](../Dockerfile)
|
||||
- [edge-plugins](../Dockerfile)
|
||||
- [edge-alpine](../Dockerfile.alpine)
|
||||
|
|
|
@ -28,10 +28,13 @@ The following placeholders are supported:
|
|||
- `{{StatusString}}`. Status as string. Possible values "OK", "KO".
|
||||
- `{{ErrorString}}`. Error details. Replaced with an empty string if no errors occur.
|
||||
- `{{VirtualPath}}`. Path seen by SFTPGo users, for example `/adir/afile.txt`.
|
||||
- `{{VirtualDirPath}}`. Parent directory for VirtualPath, for example if VirtualPath is "/adir/afile.txt", VirtualDirPath is "/adir".
|
||||
- `{{FsPath}}`. Full filesystem path, for example `/user/homedir/adir/afile.txt` or `C:/data/user/homedir/adir/afile.txt` on Windows.
|
||||
- `{{ObjectName}}`. File/directory name, for example `afile.txt` or provider object name.
|
||||
- `{{ObjectType}}`. Object type for provider events: `user`, `group`, `admin`, etc.
|
||||
- `{{VirtualTargetPath}}`. Virtual target path for renames.
|
||||
- `{{VirtualTargetDirPath}}`. Parent directory for VirtualTargetPath.
|
||||
- `{{TargetName}}`. Target object name for renames.
|
||||
- `{{FsTargetPath}}`. Full filesystem target path for renames.
|
||||
- `{{FileSize}}`. File size.
|
||||
- `{{Protocol}}`. Used protocol, for example `SFTP`, `FTP`.
|
||||
|
|
|
@ -65,7 +65,7 @@ The configuration file contains the following sections:
|
|||
- `hook`, string. Absolute path to the command to execute or HTTP URL to notify.
|
||||
- `setstat_mode`, integer. 0 means "normal mode": requests for changing permissions, owner/group and access/modification times are executed. 1 means "ignore mode": requests for changing permissions, owner/group and access/modification times are silently ignored. 2 means "ignore mode if not supported": requests for changing permissions and owner/group are silently ignored for cloud filesystems and executed for local/SFTP filesystem. Requests for changing modification times are always executed for local/SFTP filesystems and are executed for cloud based filesystems if the target is a file and there is a metadata plugin available. A metadata plugin can be found [here](https://github.com/sftpgo/sftpgo-plugin-metadata).
|
||||
- `temp_path`, string. Defines the path for temporary files such as those used for atomic uploads or file pipes. If you set this option you must make sure that the defined path exists, is accessible for writing by the user running SFTPGo, and is on the same filesystem as the users home directories otherwise the renaming for atomic uploads will become a copy and therefore may take a long time. The temporary files are not namespaced. The default is generally fine. Leave empty for the default.
|
||||
- `proxy_protocol`, integer. Support for [HAProxy PROXY protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt). If you are running SFTPGo behind a proxy server such as HAProxy, AWS ELB or NGINX, you can enable the proxy protocol. It provides a convenient way to safely transport connection information such as a client's address across multiple layers of NAT or TCP proxies to get the real client IP address instead of the proxy IP. Both protocol versions 1 and 2 are supported. If the proxy protocol is enabled in SFTPGo then you have to enable the protocol in your proxy configuration too. For example, for HAProxy, add `send-proxy` or `send-proxy-v2` to each server configuration line. The following modes are supported:
|
||||
- `proxy_protocol`, integer. Support for [HAProxy PROXY protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt). If you are running SFTPGo behind a proxy server such as HAProxy, AWS ELB or NGINX, you can enable the proxy protocol. It provides a convenient way to safely transport connection information such as a client's address across multiple layers of NAT or TCP proxies to get the real client IP address instead of the proxy IP. Both protocol versions 1 and 2 are supported. If the proxy protocol is enabled in SFTPGo then you have to enable the protocol in your proxy configuration too. For example, for HAProxy, add `send-proxy` or `send-proxy-v2` to each server configuration line. The PROXY protocol is supported for SSH/SFTP and FTP/S. The following modes are supported:
|
||||
- 0, disabled
|
||||
- 1, enabled. If the upstream IP is not allowed to send a proxy header the header be ignored. Using this mode does not mean that we can accept connections with and without the proxy header. We always try to read the proxy header and we ignore it if the upstream IP is not allowed to send a proxy header
|
||||
- 2, required. If the upstream IP is not allowed to send a proxy header the connection will be rejected
|
||||
|
@ -129,7 +129,8 @@ The configuration file contains the following sections:
|
|||
- `host_keys`, list of strings. It contains the daemon's private host keys. Each host key can be defined as a path relative to the configuration directory or an absolute one. If empty, the daemon will search or try to generate `id_rsa`, `id_ecdsa` and `id_ed25519` keys inside the configuration directory. If you configure absolute paths to files named `id_rsa`, `id_ecdsa` and/or `id_ed25519` then SFTPGo will try to generate these keys using the default settings.
|
||||
- `host_certificates`, list of strings. Public host certificates. Each certificate can be defined as a path relative to the configuration directory or an absolute one. Certificate's public key must match a private host key otherwise it will be silently ignored. Default: empty.
|
||||
- `host_key_algorithms`, list of strings. Public key algorithms that the server will accept for host key authentication. The supported values are: `rsa-sha2-512-cert-v01@openssh.com`, `rsa-sha2-256-cert-v01@openssh.com`, `ssh-rsa-cert-v01@openssh.com`, `ssh-dss-cert-v01@openssh.com`, `ecdsa-sha2-nistp256-cert-v01@openssh.com`, `ecdsa-sha2-nistp384-cert-v01@openssh.com`, `ecdsa-sha2-nistp521-cert-v01@openssh.com`, `ssh-ed25519-cert-v01@openssh.com`, `ecdsa-sha2-nistp256`, `ecdsa-sha2-nistp384`, `ecdsa-sha2-nistp521`, `rsa-sha2-512`, `rsa-sha2-256`, `ssh-rsa`, `ssh-dss`, `ssh-ed25519`. Default values: `rsa-sha2-512-cert-v01@openssh.com`, `rsa-sha2-256-cert-v01@openssh.com`, `ecdsa-sha2-nistp256-cert-v01@openssh.com`, `ecdsa-sha2-nistp384-cert-v01@openssh.com`, `ecdsa-sha2-nistp521-cert-v01@openssh.com`, `ssh-ed25519-cert-v01@openssh.com`, `ecdsa-sha2-nistp256`, `ecdsa-sha2-nistp384`, `ecdsa-sha2-nistp521`, `rsa-sha2-512`, `rsa-sha2-256`, `ssh-ed25519`.
|
||||
- `kex_algorithms`, list of strings. Available KEX (Key Exchange) algorithms in preference order. Leave empty to use default values. The supported values are: `curve25519-sha256`, `curve25519-sha256@libssh.org`, `ecdh-sha2-nistp256`, `ecdh-sha2-nistp384`, `ecdh-sha2-nistp521`, `diffie-hellman-group14-sha256`, `diffie-hellman-group16-sha512`, `diffie-hellman-group18-sha512`, `diffie-hellman-group14-sha1`, `diffie-hellman-group1-sha1`. Default values: `curve25519-sha256`, `curve25519-sha256@libssh.org`, `ecdh-sha2-nistp256`, `ecdh-sha2-nistp384`, `ecdh-sha2-nistp521`, `diffie-hellman-group14-sha256`. SHA512 based KEXs are disabled by default because they are slow.
|
||||
- `moduli`, list of strings. Diffie-Hellman moduli files. Each moduli file can be defined as a path relative to the configuration directory or an absolute one. If set, `diffie-hellman-group-exchange-sha256` and `diffie-hellman-group-exchange-sha1` KEX algorithms will be available, `diffie-hellman-group-exchange-sha256` will be enabled by default if you don't explicitly set KEXs. Default: empty.
|
||||
- `kex_algorithms`, list of strings. Available KEX (Key Exchange) algorithms in preference order. Leave empty to use default values. The supported values are: `curve25519-sha256`, `curve25519-sha256@libssh.org`, `ecdh-sha2-nistp256`, `ecdh-sha2-nistp384`, `ecdh-sha2-nistp521`, `diffie-hellman-group14-sha256`, `diffie-hellman-group16-sha512`, `diffie-hellman-group18-sha512`, `diffie-hellman-group14-sha1`, `diffie-hellman-group1-sha1`. Default values: `curve25519-sha256`, `curve25519-sha256@libssh.org`, `ecdh-sha2-nistp256`, `ecdh-sha2-nistp384`, `ecdh-sha2-nistp521`, `diffie-hellman-group14-sha256`. SHA512 based KEXs are disabled by default because they are slow. If you set one or more moduli files, `diffie-hellman-group-exchange-sha256` and `diffie-hellman-group-exchange-sha1` will be available.
|
||||
- `ciphers`, list of strings. Allowed ciphers in preference order. Leave empty to use default values. The supported values are: `aes128-gcm@openssh.com`, `aes256-gcm@openssh.com`, `chacha20-poly1305@openssh.com`, `aes128-ctr`, `aes192-ctr`, `aes256-ctr`, `aes128-cbc`, `aes192-cbc`, `aes256-cbc`, `3des-cbc`, `arcfour256`, `arcfour128`, `arcfour`. Default values: `aes128-gcm@openssh.com`, `aes256-gcm@openssh.com`, `chacha20-poly1305@openssh.com`, `aes128-ctr`, `aes192-ctr`, `aes256-ctr`. Please note that the ciphers disabled by default are insecure, you should expect that an active attacker can recover plaintext if you enable them.
|
||||
- `macs`, list of strings. Available MAC (message authentication code) algorithms in preference order. Leave empty to use default values. The supported values are: `hmac-sha2-256-etm@openssh.com`, `hmac-sha2-256`, `hmac-sha2-512-etm@openssh.com`, `hmac-sha2-512`, `hmac-sha1`, `hmac-sha1-96`. Default values: `hmac-sha2-256-etm@openssh.com`, `hmac-sha2-256`.
|
||||
- `trusted_user_ca_keys`, list of public keys paths of certificate authorities that are trusted to sign user certificates for authentication. The paths can be absolute or relative to the configuration directory.
|
||||
|
|
|
@ -24,8 +24,7 @@ If you use WebDAV behind a reverse proxy ensure to preserve the `Host` header or
|
|||
Know issues:
|
||||
|
||||
- removing a directory tree on Cloud Storage backends could generate a `not found` error when removing the last (virtual) directory. This happens if the client cycles the directories tree itself and removes files and directories one by one instead of issuing a single remove command
|
||||
- the used [WebDAV library](https://pkg.go.dev/golang.org/x/net/webdav?tab=doc) asks to open a file to execute a `stat` and sometimes reads some bytes to find the content type. Stat calls are executed before and after a download too, so to be able to properly list a directory you need to grant both `list` and `download` permissions and to be able to upload files you need to gran both `list` and `upload` permissions
|
||||
- the used [WebDAV library](https://pkg.go.dev/golang.org/x/net/webdav?tab=doc) not always returns a proper error code/message, most of the times it simply returns `Method not Allowed`. I'll try to improve the library error codes in the future
|
||||
- to be able to properly list a directory you need to grant both `list` and `download` permissions and to be able to upload files you need to gran both `list` and `upload` permissions
|
||||
- if a file or a directory cannot be accessed, for example due to OS permissions issues or because a mapped path for a virtual folder is a missing, it will be omitted from the directory listing. If there is a different error then the whole directory listing will fail. This behavior is different from SFTP/FTP where you will be able to see the problematic file/directory in the directory listing, you will only get an error if you try to access it
|
||||
- if you use the native Windows client please check its usage and pay particular attention to the [registry settings](https://docs.microsoft.com/en-us/iis/publish/using-webdav/using-the-webdav-redirector#webdav-redirector-registry-settings). The default file size limit is 50MB and if you don't configure SFTPGo to use HTTPS you have to set `BasicAuthLevel` to `2`
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# Data Backup
|
||||
|
||||
:warning: Since v2.4.0 you can use the [EventManager](../../docs/eventmanager.md) to schedule backups.
|
||||
|
||||
The `backup` example script shows how to use the SFTPGo REST API to backup your data.
|
||||
|
||||
The script is written in Python and has the following requirements:
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# File retention policies
|
||||
|
||||
:warning: Since v2.4.0 you can use the [EventManager](../../docs/eventmanager.md) to schedule data retention checks.
|
||||
|
||||
The `checkretention` example script shows how to use the SFTPGo REST API to manage data retention.
|
||||
|
||||
:warning: Deleting files is an irreversible action, please make sure you fully understand what you are doing before using this feature, you may have users with overlapping home directories or virtual folders shared between multiple users, it is relatively easy to inadvertently delete files you need.
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# Update user quota
|
||||
|
||||
:warning: Since v2.4.0 you can use the [EventManager](../../docs/eventmanager.md) to schedule quota scans.
|
||||
|
||||
The `scanuserquota` example script shows how to use the SFTPGo REST API to update the users' quota.
|
||||
|
||||
The stored quota may be incorrect for several reasons, such as an unexpected shutdown while uploading files, temporary provider failures, files copied outside of SFTPGo, and so on.
|
||||
|
|
3
fail2ban/README.md
Normal file
3
fail2ban/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Fail2ban
|
||||
|
||||
:warning: We recommend using the [built-in defender](../docs/defender.md) instead of Fail2ban.
|
93
go.mod
93
go.mod
|
@ -3,28 +3,29 @@ module github.com/drakkan/sftpgo/v2
|
|||
go 1.19
|
||||
|
||||
require (
|
||||
cloud.google.com/go/storage v1.27.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.4
|
||||
cloud.google.com/go/storage v1.28.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.2.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.5.1
|
||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
|
||||
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
|
||||
github.com/aws/aws-sdk-go-v2 v1.17.1
|
||||
github.com/aws/aws-sdk-go-v2/config v1.17.10
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.12.23
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.3
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.3
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.19
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.37
|
||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.21
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.1
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.4
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.17.1
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.2.16
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.42
|
||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.24
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.4
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.8
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.17.5
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.2.19
|
||||
github.com/coreos/go-oidc/v3 v3.4.0
|
||||
github.com/drakkan/webdav v0.0.0-20221101181759-17ed21f9337b
|
||||
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
|
||||
github.com/fclairamb/ftpserverlib v0.20.1-0.20221012093027-95be4ae0c9a6
|
||||
github.com/fclairamb/go-log v0.4.1
|
||||
github.com/go-acme/lego/v4 v4.9.0
|
||||
github.com/go-chi/chi/v5 v5.0.8-0.20221018120124-e5529d9db4d3
|
||||
github.com/go-chi/jwtauth/v5 v5.0.2
|
||||
github.com/go-chi/jwtauth/v5 v5.1.0
|
||||
github.com/go-chi/render v1.0.2
|
||||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/golang/mock v1.6.0
|
||||
|
@ -32,55 +33,55 @@ require (
|
|||
github.com/google/uuid v1.3.0
|
||||
github.com/grandcat/zeroconf v1.0.0
|
||||
github.com/hashicorp/go-hclog v1.3.1
|
||||
github.com/hashicorp/go-plugin v1.4.5
|
||||
github.com/hashicorp/go-plugin v1.4.6
|
||||
github.com/hashicorp/go-retryablehttp v0.7.1
|
||||
github.com/jackc/pgx/v5 v5.0.4
|
||||
github.com/jackc/pgx/v5 v5.1.1
|
||||
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
|
||||
github.com/klauspost/compress v1.15.12
|
||||
github.com/lestrrat-go/jwx v1.2.25
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.8
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7
|
||||
github.com/mattn/go-sqlite3 v1.14.16
|
||||
github.com/mhale/smtpd v0.8.0
|
||||
github.com/minio/sio v0.3.0
|
||||
github.com/otiai10/copy v1.7.0
|
||||
github.com/otiai10/copy v1.9.0
|
||||
github.com/pires/go-proxyproto v0.6.2
|
||||
github.com/pkg/sftp v1.13.6-0.20221020054726-e4133ab7e9bd
|
||||
github.com/pquerna/otp v1.3.0
|
||||
github.com/prometheus/client_golang v1.13.0
|
||||
github.com/prometheus/client_golang v1.14.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/rs/cors v1.8.3-0.20220619195839-da52b0701de5
|
||||
github.com/rs/xid v1.4.0
|
||||
github.com/rs/zerolog v1.28.0
|
||||
github.com/sftpgo/sdk v0.1.2
|
||||
github.com/shirou/gopsutil/v3 v3.22.9
|
||||
github.com/spf13/afero v1.9.2
|
||||
github.com/shirou/gopsutil/v3 v3.22.10
|
||||
github.com/spf13/afero v1.9.3
|
||||
github.com/spf13/cobra v1.6.1
|
||||
github.com/spf13/viper v1.13.0
|
||||
github.com/spf13/viper v1.14.0
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/studio-b12/gowebdav v0.0.0-20221015232716-17255f2e7423
|
||||
github.com/studio-b12/gowebdav v0.0.0-20221109171924-60ec5ad56012
|
||||
github.com/subosito/gotenv v1.4.1
|
||||
github.com/unrolled/secure v1.13.0
|
||||
github.com/wagslane/go-password-validator v0.3.0
|
||||
github.com/xhit/go-simple-mail/v2 v2.12.0
|
||||
github.com/xhit/go-simple-mail/v2 v2.13.0
|
||||
github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a
|
||||
go.etcd.io/bbolt v1.3.6
|
||||
go.uber.org/automaxprocs v1.5.1
|
||||
gocloud.dev v0.27.0
|
||||
golang.org/x/crypto v0.1.0
|
||||
golang.org/x/net v0.1.0
|
||||
golang.org/x/oauth2 v0.1.0
|
||||
golang.org/x/sys v0.1.0
|
||||
golang.org/x/time v0.1.0
|
||||
google.golang.org/api v0.101.0
|
||||
golang.org/x/crypto v0.3.0
|
||||
golang.org/x/net v0.2.0
|
||||
golang.org/x/oauth2 v0.2.0
|
||||
golang.org/x/sys v0.2.0
|
||||
golang.org/x/time v0.2.0
|
||||
google.golang.org/api v0.103.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.105.0 // indirect
|
||||
cloud.google.com/go v0.107.0 // indirect
|
||||
cloud.google.com/go/compute v1.12.1 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.1.1 // indirect
|
||||
cloud.google.com/go/iam v0.6.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.1 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.1 // indirect
|
||||
cloud.google.com/go/iam v0.7.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.1 // indirect
|
||||
github.com/ajg/form v1.5.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.25 // indirect
|
||||
|
@ -97,9 +98,9 @@ require (
|
|||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/boombuler/barcode v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.2.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.4.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
|
||||
|
@ -112,7 +113,7 @@ require (
|
|||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.6.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.7.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||
|
@ -120,11 +121,11 @@ require (
|
|||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.1.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.1 // indirect
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/httprc v1.0.4 // indirect
|
||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||
github.com/lestrrat-go/option v1.0.0 // indirect
|
||||
github.com/lib/pq v1.10.7 // indirect
|
||||
|
@ -139,8 +140,7 @@ require (
|
|||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/oklog/run v1.1.0 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect
|
||||
github.com/prometheus/client_model v0.3.0 // indirect
|
||||
|
@ -150,18 +150,18 @@ require (
|
|||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.10 // indirect
|
||||
github.com/tklauser/numcpus v0.5.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.11 // indirect
|
||||
github.com/tklauser/numcpus v0.6.0 // indirect
|
||||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||
go.opencensus.io v0.23.0 // indirect
|
||||
golang.org/x/mod v0.6.0 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
golang.org/x/mod v0.7.0 // indirect
|
||||
golang.org/x/text v0.4.0 // indirect
|
||||
golang.org/x/tools v0.2.0 // indirect
|
||||
golang.org/x/tools v0.3.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20221025140454-527a21cfbd71 // indirect
|
||||
google.golang.org/grpc v1.50.1 // indirect
|
||||
google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6 // indirect
|
||||
google.golang.org/grpc v1.51.0 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
|
@ -171,6 +171,5 @@ require (
|
|||
|
||||
replace (
|
||||
github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9
|
||||
golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20221020054403-a265c1cba3cb
|
||||
golang.org/x/net => github.com/drakkan/net v0.0.0-20221026175805-eaebd725b308
|
||||
golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20221117111000-a0321143587c
|
||||
)
|
||||
|
|
280
go.sum
280
go.sum
|
@ -36,8 +36,8 @@ cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w9
|
|||
cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc=
|
||||
cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU=
|
||||
cloud.google.com/go v0.103.0/go.mod h1:vwLx1nqLrzLX/fpwSMOXmFIqBOyHsvHbnAdbGSJ+mKk=
|
||||
cloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y=
|
||||
cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM=
|
||||
cloud.google.com/go v0.107.0 h1:qkj22L7bgkl6vIeZDlOY2po43Mx/TIa2Wsa7VR+PEww=
|
||||
cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
|
@ -52,18 +52,19 @@ cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLq
|
|||
cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U=
|
||||
cloud.google.com/go/compute v1.12.1 h1:gKVJMEyqV5c/UnpzjjQbo3Rjvvqpr9B1DFSbJC4OXr0=
|
||||
cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=
|
||||
cloud.google.com/go/compute/metadata v0.1.1 h1:/sxEbyrm6cw+XOUw1YxBHlatV71z4vpnmO7z2IZ0h3I=
|
||||
cloud.google.com/go/compute/metadata v0.1.1/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU=
|
||||
cloud.google.com/go/compute/metadata v0.2.1 h1:efOwf5ymceDhK6PKMnnrTHP4pppY5L22mle96M1yP48=
|
||||
cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
|
||||
cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
|
||||
cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c=
|
||||
cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY=
|
||||
cloud.google.com/go/iam v0.6.0 h1:nsqQC88kT5Iwlm4MeNGTpfMWddp6NB/UOLFTH6m1QfQ=
|
||||
cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc=
|
||||
cloud.google.com/go/kms v1.4.0 h1:iElbfoE61VeLhnZcGOltqL8HIly8Nhbe5t6JlH9GXjo=
|
||||
cloud.google.com/go/iam v0.7.0 h1:k4MuwOsS7zGJJ+QfZ5vBK8SgHBAvYN/23BWsiihJ1vs=
|
||||
cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg=
|
||||
cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA=
|
||||
cloud.google.com/go/kms v1.6.0 h1:OWRZzrPmOZUzurjI2FBGtgY2mB1WaJkqhw6oIwSj0Yg=
|
||||
cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs=
|
||||
cloud.google.com/go/monitoring v1.1.0/go.mod h1:L81pzz7HKn14QCMaCs6NTQkdBnE87TElyanS95vIcl4=
|
||||
cloud.google.com/go/monitoring v1.5.0/go.mod h1:/o9y8NYX5j91JjD/JvGLYbi86kL11OjyJXq2XziLJu4=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
|
@ -81,8 +82,8 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f
|
|||
cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y=
|
||||
cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc=
|
||||
cloud.google.com/go/storage v1.24.0/go.mod h1:3xrJEFMXBsQLgxwThyjuD3aYlroL0TMRec1ypGUQ0KE=
|
||||
cloud.google.com/go/storage v1.27.0 h1:YOO045NZI9RKfCj1c5A/ZtuuENUc8OAW+gHdGnDgyMQ=
|
||||
cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s=
|
||||
cloud.google.com/go/storage v1.28.0 h1:DLrIZ6xkeZX6K70fU/boWx5INJumt6f+nwwWSHXzzGY=
|
||||
cloud.google.com/go/storage v1.28.0/go.mod h1:qlgZML35PXA3zoEnIkiPLY4/TOkUleufRlu6qmcf7sI=
|
||||
cloud.google.com/go/trace v1.0.0/go.mod h1:4iErSByzxkyHWzzlAj63/Gmjz0NH1ASqhJguHpGcr6A=
|
||||
cloud.google.com/go/trace v1.2.0/go.mod h1:Wc8y/uYyOhPy12KEnXG9XGrvfMz5F5SrYecQlbW1rwM=
|
||||
code.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c/go.mod h1:QD9Lzhd/ux6eNQVUDVRJX/RKTigpewimNYBi7ivZKY8=
|
||||
|
@ -100,15 +101,15 @@ github.com/Azure/azure-sdk-for-go v66.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9mo
|
|||
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.1/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.4 h1:pqrAR74b6EoR4kcxF7L7Wg2B8Jgil9UUZtMvxhEFqWo=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.4/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.2.0 h1:sVW/AFBTGyJxDaMYlq0ct3jUXTtj12tQ6zE2GZUgVQw=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.2.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.0.0/go.mod h1:+6sju8gk8FRmSajX3Oz4G5Gm7P+mbqE9FVaXXFYTkCM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0 h1:QkAcEIAKbNL4KoFr4SathZPhDhF4mVwpBMFlYjyAqy8=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.1 h1:XUNQ4mw+zJmaA2KXzP9JlQiecy1SI+Eog7xVkPiqIbg=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.1/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.1 h1:Oj853U9kG+RLTCQXpjvOnrv0WaZHxgmZz1TlLywgOPY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.1/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.0.2/go.mod h1:LH9XQnMr2ZYxQdVdCrzLO9mxeDyrDFa6wbSI3x5zCZk=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.4.1/go.mod h1:eZ4g6GUvXiGulfIbbhh1Xr4XwUYaYaWMqzGD/284wCA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.5.1 h1:BMTdr+ib5ljLa9MxTJK8x/Ds0MbBb4MfuW5BL0zMJnI=
|
||||
|
@ -232,17 +233,17 @@ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.3/go.mod h1:gNsR5CaXK
|
|||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.9 h1:RKci2D7tMwpvGpDNZnGQw9wk6v7o/xSwFcUAuNPoB8k=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.9/go.mod h1:vCmV1q1VK8eoQJ5+aYE7PkK1K6v41qJ5pJdK3ggCDvg=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.15.15/go.mod h1:A1Lzyy/o21I5/s2FbyX5AevQfSVXpvvIDCoVFD0BC4E=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.17.10 h1:zBy5QQ/mkvHElM1rygHPAzuH+sl8nsdSaxSWj0+rpdE=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.17.10/go.mod h1:/4np+UiJJKpWHN7Q+LZvqXYgyjgeXm5+lLfDI6TPZao=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.3 h1:3kfBKcX3votFX84dm00U8RGA1sCCh3eRMOGzg5dCWfU=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.3/go.mod h1:BYdrbeCse3ZnOD5+2/VE/nATOK8fEUpBtmPMdKSyhMU=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.12.10/go.mod h1:g5eIM5XRs/OzIIK81QMBl+dAuDyoLN0VYaLP+tBqEOk=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.12.23 h1:LctvcJMIb8pxvk5hQhChpCu0WlU6oKQmcYb1HA4IZSA=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.12.23/go.mod h1:0awX9iRr/+UO7OwRQFpV1hNtXxOVuehpjVEzrIAYNcA=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.3 h1:ur+FHdp4NbVIv/49bUjBW+FE7e57HOo03ELodttmagk=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.3/go.mod h1:/rOMmqYBcFfNbRPU0iN9IgGqD5+V2yp3iWNmIlz0wI4=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.9/go.mod h1:KDCCm4ONIdHtUloDcFvK2+vshZvx4Zmj7UMDfusuz5s=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.19 h1:E3PXZSI3F2bzyj6XxUXdTIfvp425HHhwKsFvmzBwHgs=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.19/go.mod h1:VihW95zQpeKQWVPGkwT+2+WJNQV8UXFfMTWdU6VErL8=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.21/go.mod h1:iIYPrQ2rYfZiB/iADYlhj9HHZ9TTi6PqKQPAqygohbE=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.37 h1:e1VtTBo+cLNjres0wTlMkmwCGGRjDEkkrz3frxxcaCs=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.37/go.mod h1:kdAV1UMnCkyG6tZJUC4mHbPoRjPA3dIK0L8mnsHERiM=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.42 h1:bxgBYvvBh+W1RnNYP4ROXEB8N+HSSucDszfE7Rb+kfU=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.42/go.mod h1:LHOsygMiW/14CkFxdXxvzKyMh3jbk/QfZVaDtCbLkl8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.15/go.mod h1:pWrr2OoHlT7M/Pd2y4HV3gJyPb3qj5qMmnPkKSNPYK4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.25 h1:nBO/RFxeq/IS5G9Of+ZrgucRciie2qpLy++3UGZ+q2E=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.25/go.mod h1:Zb29PYkf42vVYQY6pvSyJCJcFHlPIiY+YKdPtwnvMkY=
|
||||
|
@ -268,14 +269,14 @@ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.9/go.mod h1:Rc5+wn2
|
|||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.19 h1:piDBAaWkaxkkVV3xJJbTehXCZRXYs49kvpi/LG6LR2o=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.19/go.mod h1:BmQWRVkLTmyNzYPFAZgon53qKLWBNSvonugD1MrSWUs=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.18.1/go.mod h1:4PZMUkc9rXHWGVB5J9vKaZy3D7Nai79ORworQ3ASMiM=
|
||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.21 h1:LpIut7TpOhp8RuTD72PBj8ksPy3+RelT3LPwGgQ8+Hg=
|
||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.21/go.mod h1:mRGY+k3s1yt7yQA3AfzJhnr68OCs1xDfQfIABFUk+ek=
|
||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.24 h1:DYr+X4xrRzcthq2OLJzsiS/uSJhZ/HHxXG0yUgGZceU=
|
||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.24/go.mod h1:mRGY+k3s1yt7yQA3AfzJhnr68OCs1xDfQfIABFUk+ek=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.27.2/go.mod h1:u+566cosFI+d+motIz3USXEh6sN8Nq4GrNXSg2RXVMo=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.1 h1:/EMdFPW/Ppieh0WUtQf1+qCGNLdsq5UWUyevBQ6vMVc=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.1/go.mod h1:/NHbqPRiwxSPVOB2Xr+StDEH+GWV/64WwnUjv4KYzV0=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.4 h1:QgmmWifaYZZcpaw3y1+ccRlgH6jAvLm4K/MBGUc7cNM=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.4/go.mod h1:/NHbqPRiwxSPVOB2Xr+StDEH+GWV/64WwnUjv4KYzV0=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.14/go.mod h1:xakbH8KMsQQKqzX87uyyzTHshc/0/Df8bsTneTS5pFU=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.4 h1:Hx79EGrkKNJya2iz2U5A7nyr7DjOu/TGTRefThfBZ1w=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.4/go.mod h1:k6CPuxyzO247nYEM1baEwHH1kRtosRCvgahAepaaShw=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.8 h1:Zw48FHykP40fKMxPmagkuzklpEuDPLhvUjKP8Ygrds0=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.8/go.mod h1:k6CPuxyzO247nYEM1baEwHH1kRtosRCvgahAepaaShw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.17.10/go.mod h1:uITsRNVMeCB3MkWpXxXw0eDz8pW4TYLzj+eyQtbhSxM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.19.1/go.mod h1:A94o564Gj+Yn+7QO1eLFeI7UVv3riy/YBFOfICVqFvU=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.27.6/go.mod h1:fiFzQgj4xNOg4/wqmAiPvzgDMXPD+cUEplX/CYn+0j0=
|
||||
|
@ -285,8 +286,8 @@ github.com/aws/aws-sdk-go-v2/service/sso v1.11.25/go.mod h1:IARHuzTXmj1C0KS35vbo
|
|||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.8 h1:jcw6kKZrtNfBPJkaHrscDOZoe5gvi9wjudnxvozYFJo=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.8/go.mod h1:er2JHN+kBY6FcMfcBBKNGCT3CarImmdFzishsqBmSRI=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.16.10/go.mod h1:cftkHYN6tCDNfkSasAmclSfl4l7cySoay8vz7p/ce0E=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.17.1 h1:KRAix/KHvjGODaHAMXnxRk9t0D+4IJVUuS/uwXxngXk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.17.1/go.mod h1:bXcN3koeVYiJcdDU89n3kCYILob7Y34AeLopUbZgLT4=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.17.5 h1:60SJ4lhvn///8ygCzYy2l53bFW/Q15bVfyjyAWo6zuw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.17.5/go.mod h1:bXcN3koeVYiJcdDU89n3kCYILob7Y34AeLopUbZgLT4=
|
||||
github.com/aws/smithy-go v1.12.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
|
||||
github.com/aws/smithy-go v1.13.4 h1:/RN2z1txIJWeXeOkzX+Hk/4Uuvv7dWtCjbmVJcrskyk=
|
||||
github.com/aws/smithy-go v1.13.4/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
|
||||
|
@ -318,8 +319,9 @@ github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEe
|
|||
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||
github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
|
||||
github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
|
||||
github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4=
|
||||
github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
|
||||
github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4=
|
||||
github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
|
||||
|
@ -358,8 +360,8 @@ github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWH
|
|||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.2.16 h1:t9dmZuC9J2W8IDQDSIGXmP+fBuEJSsrGXxWQz4cYqBY=
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.2.16/go.mod h1:xZ2VHjUEb/cySv0scXBx7YsBnHtLHkR1+w/w73b5i3M=
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.2.19 h1:YIHyz17jZumBeXPuoZKq/0nrITsqDoDD8/KQt3/xiyc=
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.2.19/go.mod h1:mzlIDDBALQfEjv/7DU12fb2AfQ/MUYTlychcMpWp9QI=
|
||||
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
|
||||
github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo=
|
||||
github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA=
|
||||
|
@ -480,8 +482,8 @@ github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+
|
|||
github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/coreos/go-systemd/v22 v22.4.0 h1:y9YHcjnjynCd/DVbg5j9L/33jQM3MxJlbj/zWskzfGU=
|
||||
github.com/coreos/go-systemd/v22 v22.4.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
|
@ -501,7 +503,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
|||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
|
||||
github.com/denisenkom/go-mssqldb v0.12.2/go.mod h1:lnIw1mZukFRZDJYQ0Pb833QS2IaC3l5HkEfra2LJ+sk=
|
||||
|
@ -537,12 +538,12 @@ github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD
|
|||
github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
|
||||
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||
github.com/drakkan/crypto v0.0.0-20221020054403-a265c1cba3cb h1:ex3x8ir969oV6bQ8fBYlaw1DXKGZCOM560bdeqeDJ/Y=
|
||||
github.com/drakkan/crypto v0.0.0-20221020054403-a265c1cba3cb/go.mod h1:IBSs4ri4rdTqz2QKcpTKpwKMdM+WJ7atZeL9lCu2swQ=
|
||||
github.com/drakkan/crypto v0.0.0-20221117111000-a0321143587c h1:3lJRXQcGw8fnheQ9ORGYE17JLuYzu6FkDuK8MZwqeRQ=
|
||||
github.com/drakkan/crypto v0.0.0-20221117111000-a0321143587c/go.mod h1:z/3TjBrBT/Dch7NfsgFg7f07WiGYZVYQgnxGHacCvRc=
|
||||
github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9 h1:LPH1dEblAOO/LoG7yHPMtBLXhQmjaga91/DDjWk9jWA=
|
||||
github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9/go.mod h1:2lmrmq866uF2tnje75wQHzmPXhmSWUt7Gyx2vgK1RCU=
|
||||
github.com/drakkan/net v0.0.0-20221026175805-eaebd725b308 h1:F7OUb3MgSa2SuY5mdmseihGXhVhxv1OCuKHrona2eh0=
|
||||
github.com/drakkan/net v0.0.0-20221026175805-eaebd725b308/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
github.com/drakkan/webdav v0.0.0-20221101181759-17ed21f9337b h1:B9z7XyDoVxLO4yEvnXgdvZ+0Uw9NA1qdD4KTSGmKcoQ=
|
||||
github.com/drakkan/webdav v0.0.0-20221101181759-17ed21f9337b/go.mod h1:8opebuqUyBXrvl7Vo/S1Zzl9U0G1X2Ceud440eVuhUE=
|
||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
|
@ -608,11 +609,10 @@ github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwv
|
|||
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
|
||||
github.com/go-acme/lego/v4 v4.9.0 h1:8Hjj44IqRS7cigshMyFQ+0pIZvwgkG/+9A0UnNh7G8A=
|
||||
github.com/go-acme/lego/v4 v4.9.0/go.mod h1:g3JRUyWS3L/VObpp4bCxzJftKyf/Wba8QrSSnoiqjg4=
|
||||
github.com/go-chi/chi/v5 v5.0.4/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/chi/v5 v5.0.8-0.20221018120124-e5529d9db4d3 h1:qzwVVqrbdP93ZaSHy0yWQRYnig+t+j1OxnVtEs8SFuQ=
|
||||
github.com/go-chi/chi/v5 v5.0.8-0.20221018120124-e5529d9db4d3/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/jwtauth/v5 v5.0.2 h1:CSKtr+b6Jnfy5T27sMaiBPxaVE/bjnjS3ramFQ0526w=
|
||||
github.com/go-chi/jwtauth/v5 v5.0.2/go.mod h1:TeA7vmPe3uYThvHw8O8W13HOOpOd4MTgToxL41gZyjs=
|
||||
github.com/go-chi/jwtauth/v5 v5.1.0 h1:wJyf2YZ/ohPvNJBwPOzZaQbyzwgMZZceE1m8FOzXLeA=
|
||||
github.com/go-chi/jwtauth/v5 v5.1.0/go.mod h1:MA93hc1au3tAQwCKry+fI4LqJ5MIVN4XSsglOo+lSc8=
|
||||
github.com/go-chi/render v1.0.2 h1:4ER/udB0+fMWB2Jlf15RV3F4A2FDuYi/9f+lFttR/Lg=
|
||||
github.com/go-chi/render v1.0.2/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
|
@ -712,8 +712,6 @@ github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY9
|
|||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/goccy/go-json v0.7.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
|
||||
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=
|
||||
|
@ -863,8 +861,8 @@ github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0
|
|||
github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=
|
||||
github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
|
||||
github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=
|
||||
github.com/googleapis/gax-go/v2 v2.6.0 h1:SXk3ABtQYDT/OH8jAyvEOQ58mgawq5C4o/4/89qN2ZU=
|
||||
github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY=
|
||||
github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ=
|
||||
github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8=
|
||||
github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
|
||||
github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU=
|
||||
github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA=
|
||||
|
@ -925,8 +923,8 @@ github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:
|
|||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-plugin v1.4.5 h1:oTE/oQR4eghggRg8VY7PAz3dr++VwDNBGCcOfIvHpBo=
|
||||
github.com/hashicorp/go-plugin v1.4.5/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s=
|
||||
github.com/hashicorp/go-plugin v1.4.6 h1:MDV3UrKQBM3du3G7MApDGvOsMYy3JQJ4exhSoKBAeVA=
|
||||
github.com/hashicorp/go-plugin v1.4.6/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
|
||||
|
@ -1015,8 +1013,8 @@ github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQ
|
|||
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
|
||||
github.com/jackc/pgx/v4 v4.16.0/go.mod h1:N0A9sFdWzkw/Jy1lwoiB64F2+ugFZi987zRxcPez/wI=
|
||||
github.com/jackc/pgx/v4 v4.16.1/go.mod h1:SIhx0D5hoADaiXZVyv+3gSm3LCIIINTVO0PficsvWGQ=
|
||||
github.com/jackc/pgx/v5 v5.0.4 h1:r5O6y84qHX/z/HZV40JBdx2obsHz7/uRj5b+CcYEdeY=
|
||||
github.com/jackc/pgx/v5 v5.0.4/go.mod h1:U0ynklHtgg43fue9Ly30w3OCSTDPlXjig9ghrNGaguQ=
|
||||
github.com/jackc/pgx/v5 v5.1.1 h1:pZD79K1SYv8wc2HmCQA6VdmRQi7/OtCfv9bM3WAXUYA=
|
||||
github.com/jackc/pgx/v5 v5.1.1/go.mod h1:Ptn7zmohNsWEsdxRawMzk3gaKma2obW+NWTnKa0S4nk=
|
||||
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
|
@ -1065,8 +1063,8 @@ github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47e
|
|||
github.com/klauspost/compress v1.15.12 h1:YClS/PImqYbn+UILDnqxQCZ3RehC9N318SU3kElDUEM=
|
||||
github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
|
||||
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.1.2 h1:XhdX4fqAJUA0yj+kUwMavO0hHrSPAecYdYf1ZmxHvak=
|
||||
github.com/klauspost/cpuid/v2 v2.1.2/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/klauspost/cpuid/v2 v2.2.1 h1:U33DW0aiEj633gHYw3LoDNfkDiYnE5Q8M/TKJn2f2jI=
|
||||
github.com/klauspost/cpuid/v2 v2.2.1/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/kolo/xmlrpc v0.0.0-20201022064351-38db28db192b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
|
@ -1089,21 +1087,16 @@ github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LE
|
|||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A=
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
|
||||
github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
|
||||
github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
|
||||
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
|
||||
github.com/lestrrat-go/codegen v1.0.1/go.mod h1:JhJw6OQAuPEfVKUCLItpaVLumDGWQznd1VaXrBk9TdM=
|
||||
github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE=
|
||||
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
|
||||
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
|
||||
github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8=
|
||||
github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
|
||||
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
|
||||
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
|
||||
github.com/lestrrat-go/jwx v1.2.6/go.mod h1:tJuGuAI3LC71IicTx82Mz1n3w9woAs2bYJZpkjJQ5aU=
|
||||
github.com/lestrrat-go/jwx v1.2.25 h1:tAx93jN2SdPvFn08fHNAhqFJazn5mBBOB8Zli0g0otA=
|
||||
github.com/lestrrat-go/jwx v1.2.25/go.mod h1:zoNuZymNl5lgdcu6P7K6ie2QRll5HVfF4xwxBBK1NxY=
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.8 h1:jCFT8oc0hEDVjgUgsBy1F9cbjsjAVZSXNi7JaU9HR/Q=
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.8/go.mod h1:zLxnyv9rTlEvOUHbc48FAfIL8iYu2hHvIRaTFGc8mT0=
|
||||
github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
|
||||
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
|
@ -1307,13 +1300,13 @@ github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxS
|
|||
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
||||
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
||||
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
||||
github.com/otiai10/copy v1.7.0 h1:hVoPiN+t+7d2nzzwMiDHPSOogsWAStewq3TwU05+clE=
|
||||
github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U=
|
||||
github.com/otiai10/copy v1.9.0 h1:7KFNiCgZ91Ru4qW4CWPf/7jqtxLagGRmIxWldPP9VY4=
|
||||
github.com/otiai10/copy v1.9.0/go.mod h1:hsfX19wcn0UWIHUQ3/4fHuehhk2UyArQ9dVFAn3FczI=
|
||||
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
|
||||
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
|
||||
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
|
||||
github.com/otiai10/mint v1.3.3 h1:7JgpsBaN0uMkyju4tbYHu0mnM55hNKVYLsXmwr15NQI=
|
||||
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
|
||||
github.com/otiai10/mint v1.4.0 h1:umwcf7gbpEwf7WFzqmWwSv0CzbeMsae2u9ZvpP8j2q4=
|
||||
github.com/otiai10/mint v1.4.0/go.mod h1:gifjb2MYOoULtKLqUAEILUG/9KONW6f7YsJ6vQLTlFI=
|
||||
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
|
@ -1324,8 +1317,8 @@ github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrap
|
|||
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg=
|
||||
github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
|
||||
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
|
||||
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
||||
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
|
||||
|
@ -1370,8 +1363,8 @@ github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP
|
|||
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
||||
github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
||||
github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU=
|
||||
github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ=
|
||||
github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
|
||||
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
|
||||
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
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=
|
||||
|
@ -1458,8 +1451,8 @@ github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod
|
|||
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=
|
||||
github.com/sftpgo/sdk v0.1.2 h1:j4V63RuVcYfJAOWV0zRUofa1PlQvKU2ujly0lB7quVA=
|
||||
github.com/sftpgo/sdk v0.1.2/go.mod h1:PTp1TfXa+95wHw9yuZu7BA3vmzLqbRkz3gBmMNnwFQg=
|
||||
github.com/shirou/gopsutil/v3 v3.22.9 h1:yibtJhIVEMcdw+tCTbOPiF1VcsuDeTE4utJ8Dm4c5eA=
|
||||
github.com/shirou/gopsutil/v3 v3.22.9/go.mod h1:bBYl1kjgEJpWpxeHmLI+dVHWtyAwfcmSBLDsp2TNT8A=
|
||||
github.com/shirou/gopsutil/v3 v3.22.10 h1:4KMHdfBRYXGF9skjDWiL4RA2N+E8dRdodU/bOZpPoVg=
|
||||
github.com/shirou/gopsutil/v3 v3.22.10/go.mod h1:QNza6r4YQoydyCfo6rH0blGfKahgibh4dQmV5xdFkQk=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
|
@ -1486,8 +1479,8 @@ github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B
|
|||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
|
||||
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
|
||||
github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw=
|
||||
github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
|
||||
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
|
||||
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
|
||||
|
@ -1509,8 +1502,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
|||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||
github.com/spf13/viper v1.13.0 h1:BWSJ/M+f+3nmdz9bxB+bWX28kkALN2ok11D0rSo8EJU=
|
||||
github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw=
|
||||
github.com/spf13/viper v1.14.0 h1:Rg7d3Lo706X9tHsJMUjdiwMpHB7W8WnSVOssIY+JElU=
|
||||
github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As=
|
||||
github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8=
|
||||
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
|
||||
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
||||
|
@ -1535,8 +1528,8 @@ github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
|||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/studio-b12/gowebdav v0.0.0-20221015232716-17255f2e7423 h1:Wd8WDEEusB5+En4PiRWJp1cP59QLNsQun+mOTW8+s6s=
|
||||
github.com/studio-b12/gowebdav v0.0.0-20221015232716-17255f2e7423/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
|
||||
github.com/studio-b12/gowebdav v0.0.0-20221109171924-60ec5ad56012 h1:ZC+dlnsjxqrcB68nEFbIEfo4iXsog3Sg8FlXKytAjhY=
|
||||
github.com/studio-b12/gowebdav v0.0.0-20221109171924-60ec5ad56012/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
|
||||
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||
|
@ -1546,11 +1539,12 @@ github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG
|
|||
github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I=
|
||||
github.com/tedsuo/ifrit v0.0.0-20180802180643-bea94bb476cc/go.mod h1:eyZnKCc955uh98WQvzOm0dgAeLnf2O0Rz0LPoC5ze+0=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw=
|
||||
github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk=
|
||||
github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
|
||||
github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
|
||||
github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ=
|
||||
github.com/tklauser/numcpus v0.5.0 h1:ooe7gN0fg6myJ0EKoTAf5hebTZrH52px3New/D9iJ+A=
|
||||
github.com/tklauser/numcpus v0.5.0/go.mod h1:OGzpTxpcIMNGYQdit2BYL1pvk/dSOaJWjKoflh+RQjo=
|
||||
github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms=
|
||||
github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
|
@ -1586,8 +1580,8 @@ github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6
|
|||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
|
||||
github.com/xhit/go-simple-mail/v2 v2.12.0 h1:KweA6NO8Z6fZyeckMPNpvElU6QDIyBShlpce1sYUZgg=
|
||||
github.com/xhit/go-simple-mail/v2 v2.12.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
|
||||
github.com/xhit/go-simple-mail/v2 v2.13.0 h1:OANWU9jHZrVfBkNkvLf8Ww0fexwpQVF/v/5f96fFTLI=
|
||||
github.com/xhit/go-simple-mail/v2 v2.13.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
|
@ -1634,8 +1628,9 @@ 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.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
|
||||
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
|
||||
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 v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.28.0/go.mod h1:vEhqr0m4eTc+DWxfsXoXue2GBgV2uUwVznkGIHW/e5w=
|
||||
|
@ -1743,8 +1738,86 @@ golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
|||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I=
|
||||
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
|
||||
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
|
||||
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/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-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
|
||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220802222814-0bcc04d9c69b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
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=
|
||||
|
@ -1771,8 +1844,8 @@ golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7Lm
|
|||
golang.org/x/oauth2 v0.0.0-20220628200809-02e64fa58f26/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
|
||||
golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
|
||||
golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
|
||||
golang.org/x/oauth2 v0.1.0 h1:isLCZuhj4v+tYv7eskaN4v/TM+A1begWWgyVJDdl1+Y=
|
||||
golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A=
|
||||
golang.org/x/oauth2 v0.2.0 h1:GtQkldQ9m7yvzCL1V+LrYow3Khe0eJH0w7RbX/VbaIU=
|
||||
golang.org/x/oauth2 v0.2.0/go.mod h1:Cwn6afJ8jrQwYMxQDTpISoXmXW9I6qF6vDeuuoX3Ibs=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
@ -1862,6 +1935,7 @@ golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -1876,9 +1950,13 @@ golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210503080704-8803ae5d1324/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -1923,18 +2001,20 @@ golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
@ -1958,8 +2038,8 @@ golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxb
|
|||
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA=
|
||||
golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.2.0 h1:52I/1L54xyEQAYdtcSuxtiT84KGYTBGXwayxmIpNJhE=
|
||||
golang.org/x/time v0.2.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
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=
|
||||
|
@ -2027,7 +2107,6 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc
|
|||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
||||
golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
|
@ -2035,7 +2114,6 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f
|
|||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
|
@ -2047,8 +2125,8 @@ golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
|||
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||
golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE=
|
||||
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
|
||||
golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM=
|
||||
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
@ -2108,8 +2186,8 @@ google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6F
|
|||
google.golang.org/api v0.86.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
|
||||
google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
|
||||
google.golang.org/api v0.91.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
|
||||
google.golang.org/api v0.101.0 h1:lJPPeEBIRxGpGLwnBTam1NPEM8Z2BmmXEd3z812pjwM=
|
||||
google.golang.org/api v0.101.0/go.mod h1:CjxAAWWt3A3VrUE2IGDY2bgK5qhoG/OkyWVlYcP05MY=
|
||||
google.golang.org/api v0.103.0 h1:9yuVqlu2JCvcLg9p8S3fcFLZij8EPSyvODIY1rkMizQ=
|
||||
google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
|
@ -2220,8 +2298,8 @@ google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljW
|
|||
google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
|
||||
google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
|
||||
google.golang.org/genproto v0.0.0-20220802133213-ce4fa296bf78/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc=
|
||||
google.golang.org/genproto v0.0.0-20221025140454-527a21cfbd71 h1:GEgb2jF5zxsFJpJfg9RoDDWm7tiwc/DDSTE2BtLUkXU=
|
||||
google.golang.org/genproto v0.0.0-20221025140454-527a21cfbd71/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s=
|
||||
google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6 h1:a2S6M0+660BgMNl++4JPlcAO/CjkqYItDEZwkoDQK7c=
|
||||
google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
|
||||
google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
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=
|
||||
|
@ -2263,8 +2341,8 @@ google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACu
|
|||
google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
|
||||
google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
|
||||
google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
|
||||
google.golang.org/grpc v1.50.1 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY=
|
||||
google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
|
||||
google.golang.org/grpc v1.51.0 h1:E1eGv1FTqoLIdnBCZufiSHgKjlqG6fKFf6pPWtMTh8U=
|
||||
google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
||||
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=
|
||||
|
|
|
@ -65,6 +65,12 @@ Please take a look at the usage below to customize the options.`,
|
|||
logger.ErrorToConsole("Unable to initialize KMS: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
mfaConfig := config.GetMFAConfig()
|
||||
err = mfaConfig.Initialize()
|
||||
if err != nil {
|
||||
logger.ErrorToConsole("Unable to initialize MFA: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
providerConf := config.GetProviderConf()
|
||||
// ignore actions
|
||||
providerConf.Actions.Hook = ""
|
||||
|
|
|
@ -309,7 +309,7 @@ func (c *BaseConnection) ListDir(virtualPath string) ([]os.FileInfo, error) {
|
|||
|
||||
// CheckParentDirs tries to create the specified directory and any missing parent dirs
|
||||
func (c *BaseConnection) CheckParentDirs(virtualPath string) error {
|
||||
fs, err := c.User.GetFilesystemForPath(virtualPath, "")
|
||||
fs, err := c.User.GetFilesystemForPath(virtualPath, c.GetID())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -321,7 +321,7 @@ func (c *BaseConnection) CheckParentDirs(virtualPath string) error {
|
|||
}
|
||||
dirs := util.GetDirsForVirtualPath(virtualPath)
|
||||
for idx := len(dirs) - 1; idx >= 0; idx-- {
|
||||
fs, err = c.User.GetFilesystemForPath(dirs[idx], "")
|
||||
fs, err = c.User.GetFilesystemForPath(dirs[idx], c.GetID())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -743,7 +743,7 @@ func (c *BaseConnection) doStatInternal(virtualPath string, mode int, checkFileP
|
|||
if _, ok := vfolders[virtualPath]; ok {
|
||||
return vfs.NewFileInfo(virtualPath, true, 0, time.Unix(0, 0), false), nil
|
||||
}
|
||||
if checkFilePatterns {
|
||||
if checkFilePatterns && virtualPath != "/" {
|
||||
ok, policy := c.User.IsFileAllowed(virtualPath)
|
||||
if !ok && policy == sdk.DenyPolicyHide {
|
||||
return nil, c.GetNotExistError()
|
||||
|
@ -1509,6 +1509,7 @@ func (c *BaseConnection) GetGenericError(err error) error {
|
|||
err == ErrQuotaExceeded || err == vfs.ErrStorageSizeUnavailable || err == ErrShuttingDown {
|
||||
return err
|
||||
}
|
||||
c.Log(logger.LevelError, "generic error: %+v", err)
|
||||
return ErrGenericFailure
|
||||
}
|
||||
}
|
||||
|
@ -1536,7 +1537,7 @@ func (c *BaseConnection) GetFsAndResolvedPath(virtualPath string) (vfs.Fs, strin
|
|||
// will not be listed
|
||||
return nil, "", c.GetPermissionDeniedError()
|
||||
}
|
||||
return nil, "", err
|
||||
return nil, "", c.GetGenericError(err)
|
||||
}
|
||||
|
||||
if isShuttingDown.Load() {
|
||||
|
|
|
@ -29,6 +29,7 @@ import (
|
|||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
@ -476,6 +477,21 @@ func (p *EventParams) AddError(err error) {
|
|||
p.errors = append(p.errors, err.Error())
|
||||
}
|
||||
|
||||
func (p *EventParams) setBackupParams(backupPath string) {
|
||||
if p.sender != "" {
|
||||
return
|
||||
}
|
||||
p.sender = dataprovider.ActionExecutorSystem
|
||||
p.FsPath = backupPath
|
||||
p.ObjectName = filepath.Base(backupPath)
|
||||
p.VirtualPath = "/" + p.ObjectName
|
||||
p.Timestamp = time.Now().UnixNano()
|
||||
info, err := os.Stat(backupPath)
|
||||
if err == nil {
|
||||
p.FileSize = info.Size()
|
||||
}
|
||||
}
|
||||
|
||||
func (p *EventParams) getStatusString() string {
|
||||
switch p.Status {
|
||||
case 1:
|
||||
|
@ -503,6 +519,18 @@ func (p *EventParams) getUsers() ([]dataprovider.User, error) {
|
|||
}
|
||||
|
||||
func (p *EventParams) getUserFromSender() (dataprovider.User, error) {
|
||||
if p.sender == dataprovider.ActionExecutorSystem {
|
||||
return dataprovider.User{
|
||||
BaseUser: sdk.BaseUser{
|
||||
Status: 1,
|
||||
Username: p.sender,
|
||||
HomeDir: dataprovider.GetBackupsPath(),
|
||||
Permissions: map[string][]string{
|
||||
"/": {dataprovider.PermAny},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
user, err := dataprovider.UserExists(p.sender)
|
||||
if err != nil {
|
||||
eventManagerLog(logger.LevelError, "unable to get user %q: %+v", p.sender, err)
|
||||
|
@ -585,6 +613,13 @@ func (p *EventParams) getStringReplacements(addObjectData bool) []string {
|
|||
"{{Timestamp}}", fmt.Sprintf("%d", p.Timestamp),
|
||||
"{{StatusString}}", p.getStatusString(),
|
||||
}
|
||||
if p.VirtualPath != "" {
|
||||
replacements = append(replacements, "{{VirtualDirPath}}", path.Dir(p.VirtualPath))
|
||||
}
|
||||
if p.VirtualTargetPath != "" {
|
||||
replacements = append(replacements, "{{VirtualTargetDirPath}}", path.Dir(p.VirtualTargetPath))
|
||||
replacements = append(replacements, "{{TargetName}}", path.Base(p.VirtualTargetPath))
|
||||
}
|
||||
if len(p.errors) > 0 {
|
||||
replacements = append(replacements, "{{ErrorString}}", strings.Join(p.errors, ", "))
|
||||
} else {
|
||||
|
@ -1903,7 +1938,11 @@ func executeRuleAction(action dataprovider.BaseEventAction, params *EventParams,
|
|||
case dataprovider.ActionTypeEmail:
|
||||
err = executeEmailRuleAction(action.Options.EmailConfig, params)
|
||||
case dataprovider.ActionTypeBackup:
|
||||
err = dataprovider.ExecuteBackup()
|
||||
var backupPath string
|
||||
backupPath, err = dataprovider.ExecuteBackup()
|
||||
if err == nil {
|
||||
params.setBackupParams(backupPath)
|
||||
}
|
||||
case dataprovider.ActionTypeUserQuotaReset:
|
||||
err = executeUsersQuotaResetRuleAction(conditions, params)
|
||||
case dataprovider.ActionTypeFolderQuotaReset:
|
||||
|
|
|
@ -951,6 +951,74 @@ func TestHiddenPatternFilter(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestHiddenRoot(t *testing.T) {
|
||||
// only the "/ftp" directory is allowed and visibile in the "/" path
|
||||
// within /ftp any file/directory is allowed and visibile
|
||||
u := getTestUser()
|
||||
u.Filters.FilePatterns = []sdk.PatternsFilter{
|
||||
{
|
||||
Path: "/",
|
||||
AllowedPatterns: []string{"ftp"},
|
||||
DenyPolicy: sdk.DenyPolicyHide,
|
||||
},
|
||||
{
|
||||
Path: "/ftp",
|
||||
AllowedPatterns: []string{"*"},
|
||||
},
|
||||
}
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
err = os.MkdirAll(filepath.Join(user.HomeDir, fmt.Sprintf("ftp%d", i)), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
err = os.WriteFile(filepath.Join(user.HomeDir, testFileName), []byte(""), 0666)
|
||||
assert.NoError(t, err)
|
||||
err = os.WriteFile(filepath.Join(user.HomeDir, "ftp.txt"), []byte(""), 0666)
|
||||
assert.NoError(t, err)
|
||||
conn, client, err := getSftpClient(user)
|
||||
if assert.NoError(t, err) {
|
||||
defer conn.Close()
|
||||
defer client.Close()
|
||||
|
||||
err = client.Mkdir("ftp")
|
||||
assert.NoError(t, err)
|
||||
entries, err := client.ReadDir("/")
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, entries, 1) {
|
||||
assert.Equal(t, "ftp", entries[0].Name())
|
||||
}
|
||||
_, err = client.Stat(".")
|
||||
assert.NoError(t, err)
|
||||
for _, name := range []string{testFileName, "ftp.txt"} {
|
||||
_, err = client.Stat(name)
|
||||
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||
}
|
||||
for i := 0; i < 10; i++ {
|
||||
_, err = client.Stat(fmt.Sprintf("ftp%d", i))
|
||||
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||
}
|
||||
err = writeSFTPFile(testFileName, 4096, client)
|
||||
assert.ErrorIs(t, err, os.ErrPermission)
|
||||
err = writeSFTPFile("ftp123", 4096, client)
|
||||
assert.ErrorIs(t, err, os.ErrPermission)
|
||||
err = client.Rename(testFileName, testFileName+"_rename")
|
||||
assert.ErrorIs(t, err, os.ErrPermission)
|
||||
err = writeSFTPFile(path.Join("/ftp", testFileName), 4096, client)
|
||||
assert.NoError(t, err)
|
||||
err = client.Mkdir("/ftp/dir")
|
||||
assert.NoError(t, err)
|
||||
err = client.Rename(path.Join("/ftp", testFileName), path.Join("/ftp/dir", testFileName))
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestFileNotAllowedErrors(t *testing.T) {
|
||||
deniedDir := "/denied"
|
||||
u := getTestUser()
|
||||
|
@ -3353,6 +3421,10 @@ func TestEventRule(t *testing.T) {
|
|||
}
|
||||
a2 := dataprovider.BaseEventAction{
|
||||
Name: "action2",
|
||||
Type: dataprovider.ActionTypeBackup,
|
||||
}
|
||||
a3 := dataprovider.BaseEventAction{
|
||||
Name: "action3",
|
||||
Type: dataprovider.ActionTypeEmail,
|
||||
Options: dataprovider.BaseEventActionOptions{
|
||||
EmailConfig: dataprovider.EventActionEmailConfig{
|
||||
|
@ -3362,8 +3434,8 @@ func TestEventRule(t *testing.T) {
|
|||
},
|
||||
},
|
||||
}
|
||||
a3 := dataprovider.BaseEventAction{
|
||||
Name: "action3",
|
||||
a4 := dataprovider.BaseEventAction{
|
||||
Name: "action4",
|
||||
Type: dataprovider.ActionTypeEmail,
|
||||
Options: dataprovider.BaseEventActionOptions{
|
||||
EmailConfig: dataprovider.EventActionEmailConfig{
|
||||
|
@ -3379,6 +3451,9 @@ func TestEventRule(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
action3, _, err := httpdtest.AddEventAction(a3, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
action4, _, err := httpdtest.AddEventAction(a4, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
||||
r1 := dataprovider.EventRule{
|
||||
Name: "test rule1",
|
||||
Trigger: dataprovider.EventTriggerFsEvent,
|
||||
|
@ -3417,6 +3492,12 @@ func TestEventRule(t *testing.T) {
|
|||
Name: action3.Name,
|
||||
},
|
||||
Order: 3,
|
||||
},
|
||||
{
|
||||
BaseEventAction: dataprovider.BaseEventAction{
|
||||
Name: action4.Name,
|
||||
},
|
||||
Order: 4,
|
||||
Options: dataprovider.EventActionOptions{
|
||||
IsFailureAction: true,
|
||||
},
|
||||
|
@ -3442,13 +3523,13 @@ func TestEventRule(t *testing.T) {
|
|||
Actions: []dataprovider.EventAction{
|
||||
{
|
||||
BaseEventAction: dataprovider.BaseEventAction{
|
||||
Name: action2.Name,
|
||||
Name: action3.Name,
|
||||
},
|
||||
Order: 1,
|
||||
},
|
||||
{
|
||||
BaseEventAction: dataprovider.BaseEventAction{
|
||||
Name: action3.Name,
|
||||
Name: action4.Name,
|
||||
},
|
||||
Order: 2,
|
||||
Options: dataprovider.EventActionOptions{
|
||||
|
@ -3469,7 +3550,7 @@ func TestEventRule(t *testing.T) {
|
|||
Actions: []dataprovider.EventAction{
|
||||
{
|
||||
BaseEventAction: dataprovider.BaseEventAction{
|
||||
Name: action2.Name,
|
||||
Name: action3.Name,
|
||||
},
|
||||
Order: 1,
|
||||
},
|
||||
|
@ -3645,6 +3726,8 @@ func TestEventRule(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveEventAction(action3, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveEventAction(action4, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
lastReceivedEmail.reset()
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
@ -3853,7 +3936,7 @@ func TestEventRuleFsActions(t *testing.T) {
|
|||
Type: dataprovider.FilesystemActionRename,
|
||||
Renames: []dataprovider.KeyValue{
|
||||
{
|
||||
Key: "/{{VirtualPath}}",
|
||||
Key: "/{{VirtualDirPath}}/{{ObjectName}}",
|
||||
Value: "/{{ObjectName}}_renamed",
|
||||
},
|
||||
},
|
||||
|
@ -4237,6 +4320,89 @@ func TestEventFsActionsGroupFilters(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestBackupAsAttachment(t *testing.T) {
|
||||
smtpCfg := smtp.Config{
|
||||
Host: "127.0.0.1",
|
||||
Port: 2525,
|
||||
From: "notification@example.com",
|
||||
TemplatesPath: "templates",
|
||||
}
|
||||
err := smtpCfg.Initialize(configDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
a1 := dataprovider.BaseEventAction{
|
||||
Name: "a1",
|
||||
Type: dataprovider.ActionTypeBackup,
|
||||
}
|
||||
a2 := dataprovider.BaseEventAction{
|
||||
Name: "a2",
|
||||
Type: dataprovider.ActionTypeEmail,
|
||||
Options: dataprovider.BaseEventActionOptions{
|
||||
EmailConfig: dataprovider.EventActionEmailConfig{
|
||||
Recipients: []string{"test@example.com"},
|
||||
Subject: `"{{Event}} {{StatusString}}"`,
|
||||
Body: "Domain: {{Name}}",
|
||||
Attachments: []string{"/{{VirtualPath}}"},
|
||||
},
|
||||
},
|
||||
}
|
||||
action1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
action2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
||||
r1 := dataprovider.EventRule{
|
||||
Name: "test rule certificate",
|
||||
Trigger: dataprovider.EventTriggerCertificate,
|
||||
Actions: []dataprovider.EventAction{
|
||||
{
|
||||
BaseEventAction: dataprovider.BaseEventAction{
|
||||
Name: action1.Name,
|
||||
},
|
||||
Order: 1,
|
||||
},
|
||||
{
|
||||
BaseEventAction: dataprovider.BaseEventAction{
|
||||
Name: action2.Name,
|
||||
},
|
||||
Order: 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
rule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
||||
lastReceivedEmail.reset()
|
||||
renewalEvent := "Certificate renewal"
|
||||
|
||||
common.HandleCertificateEvent(common.EventParams{
|
||||
Name: "example.com",
|
||||
Timestamp: time.Now().UnixNano(),
|
||||
Status: 1,
|
||||
Event: renewalEvent,
|
||||
})
|
||||
assert.Eventually(t, func() bool {
|
||||
return lastReceivedEmail.get().From != ""
|
||||
}, 3000*time.Millisecond, 100*time.Millisecond)
|
||||
email := lastReceivedEmail.get()
|
||||
assert.Len(t, email.To, 1)
|
||||
assert.True(t, util.Contains(email.To, "test@example.com"))
|
||||
assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "%s OK"`, renewalEvent))
|
||||
assert.Contains(t, email.Data, `Domain: example.com`)
|
||||
assert.Contains(t, email.Data, "Content-Type: application/json")
|
||||
|
||||
_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveEventAction(action2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
||||
smtpCfg = smtp.Config{}
|
||||
err = smtpCfg.Initialize(configDir)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestEventActionHTTPMultipart(t *testing.T) {
|
||||
a1 := dataprovider.BaseEventAction{
|
||||
Name: "action1",
|
||||
|
@ -5196,6 +5362,86 @@ func TestEventRuleFirstUploadDownloadActions(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestEventRuleRenameEvent(t *testing.T) {
|
||||
smtpCfg := smtp.Config{
|
||||
Host: "127.0.0.1",
|
||||
Port: 2525,
|
||||
From: "notify@example.com",
|
||||
TemplatesPath: "templates",
|
||||
}
|
||||
err := smtpCfg.Initialize(configDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
a1 := dataprovider.BaseEventAction{
|
||||
Name: "action1",
|
||||
Type: dataprovider.ActionTypeEmail,
|
||||
Options: dataprovider.BaseEventActionOptions{
|
||||
EmailConfig: dataprovider.EventActionEmailConfig{
|
||||
Recipients: []string{"test@example.com"},
|
||||
Subject: `"{{Event}}" from "{{Name}}"`,
|
||||
Body: `Fs path {{FsPath}}, Target path "{{VirtualTargetDirPath}}/{{TargetName}}", size: {{FileSize}}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
action1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
r1 := dataprovider.EventRule{
|
||||
Name: "test rename rule",
|
||||
Trigger: dataprovider.EventTriggerFsEvent,
|
||||
Conditions: dataprovider.EventConditions{
|
||||
FsEvents: []string{"rename"},
|
||||
},
|
||||
Actions: []dataprovider.EventAction{
|
||||
{
|
||||
BaseEventAction: dataprovider.BaseEventAction{
|
||||
Name: action1.Name,
|
||||
},
|
||||
Order: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
rule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
||||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
conn, client, err := getSftpClient(user)
|
||||
if assert.NoError(t, err) {
|
||||
defer conn.Close()
|
||||
defer client.Close()
|
||||
|
||||
testFileSize := int64(32768)
|
||||
lastReceivedEmail.reset()
|
||||
err = writeSFTPFileNoCheck(testFileName, testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
err = client.Mkdir("subdir")
|
||||
assert.NoError(t, err)
|
||||
err = client.Rename(testFileName, path.Join("/subdir", testFileName))
|
||||
assert.NoError(t, err)
|
||||
assert.Eventually(t, func() bool {
|
||||
return lastReceivedEmail.get().From != ""
|
||||
}, 1500*time.Millisecond, 100*time.Millisecond)
|
||||
email := lastReceivedEmail.get()
|
||||
assert.Len(t, email.To, 1)
|
||||
assert.True(t, util.Contains(email.To, "test@example.com"))
|
||||
assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "rename" from "%s"`, user.Username))
|
||||
assert.Contains(t, email.Data, fmt.Sprintf("Target path %q", path.Join("/subdir", testFileName)))
|
||||
}
|
||||
|
||||
_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveEventAction(action1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
||||
smtpCfg = smtp.Config{}
|
||||
err = smtpCfg.Initialize(configDir)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestEventRuleCertificate(t *testing.T) {
|
||||
smtpCfg := smtp.Config{
|
||||
Host: "127.0.0.1",
|
||||
|
@ -6141,14 +6387,10 @@ func TestSFTPLoopError(t *testing.T) {
|
|||
|
||||
conn = common.NewBaseConnection("", common.ProtocolSFTP, "", "", user1)
|
||||
_, _, err = conn.GetFsAndResolvedPath(user1.VirtualFolders[0].VirtualPath)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "SFTP loop")
|
||||
}
|
||||
assert.Error(t, err)
|
||||
conn = common.NewBaseConnection("", common.ProtocolFTP, "", "", user1)
|
||||
_, _, err = conn.GetFsAndResolvedPath(user1.VirtualFolders[0].VirtualPath)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "SFTP loop")
|
||||
}
|
||||
assert.Error(t, err)
|
||||
|
||||
_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
|
|
@ -253,6 +253,7 @@ func Init() {
|
|||
HostKeys: []string{},
|
||||
HostCertificates: []string{},
|
||||
HostKeyAlgorithms: []string{},
|
||||
Moduli: []string{},
|
||||
KexAlgorithms: []string{},
|
||||
Ciphers: []string{},
|
||||
MACs: []string{},
|
||||
|
@ -1961,6 +1962,7 @@ func setViperDefaults() {
|
|||
viper.SetDefault("sftpd.host_keys", globalConf.SFTPD.HostKeys)
|
||||
viper.SetDefault("sftpd.host_certificates", globalConf.SFTPD.HostCertificates)
|
||||
viper.SetDefault("sftpd.host_key_algorithms", globalConf.SFTPD.HostKeyAlgorithms)
|
||||
viper.SetDefault("sftpd.moduli", globalConf.SFTPD.Moduli)
|
||||
viper.SetDefault("sftpd.kex_algorithms", globalConf.SFTPD.KexAlgorithms)
|
||||
viper.SetDefault("sftpd.ciphers", globalConf.SFTPD.Ciphers)
|
||||
viper.SetDefault("sftpd.macs", globalConf.SFTPD.MACs)
|
||||
|
|
|
@ -15,14 +15,13 @@
|
|||
package dataprovider
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alexedwards/argon2id"
|
||||
|
@ -71,9 +70,9 @@ const (
|
|||
var (
|
||||
validAdminPerms = []string{PermAdminAny, PermAdminAddUsers, PermAdminChangeUsers, PermAdminDeleteUsers,
|
||||
PermAdminViewUsers, PermAdminManageGroups, PermAdminViewConnections, PermAdminCloseConnections,
|
||||
PermAdminViewServerStatus, PermAdminManageAdmins, PermAdminManageAPIKeys, PermAdminQuotaScans,
|
||||
PermAdminManageSystem, PermAdminManageDefender, PermAdminViewDefender, PermAdminRetentionChecks,
|
||||
PermAdminMetadataChecks, PermAdminViewEvents}
|
||||
PermAdminViewServerStatus, PermAdminManageAdmins, PermAdminManageEventRules, PermAdminManageAPIKeys,
|
||||
PermAdminQuotaScans, PermAdminManageSystem, PermAdminManageDefender, PermAdminViewDefender,
|
||||
PermAdminRetentionChecks, PermAdminMetadataChecks, PermAdminViewEvents}
|
||||
)
|
||||
|
||||
// AdminTOTPConfig defines the time-based one time password configuration
|
||||
|
@ -548,12 +547,9 @@ func (a *Admin) CanManageMFA() bool {
|
|||
}
|
||||
|
||||
// GetSignature returns a signature for this admin.
|
||||
// It could change after an update
|
||||
// It will change after an update
|
||||
func (a *Admin) GetSignature() string {
|
||||
data := []byte(a.Username)
|
||||
data = append(data, []byte(a.Password)...)
|
||||
signature := sha256.Sum256(data)
|
||||
return base64.StdEncoding.EncodeToString(signature[:])
|
||||
return strconv.FormatInt(a.UpdatedAt, 10)
|
||||
}
|
||||
|
||||
func (a *Admin) getACopy() Admin {
|
||||
|
|
|
@ -18,7 +18,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/webdav"
|
||||
"github.com/drakkan/webdav"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/logger"
|
||||
"github.com/drakkan/sftpgo/v2/internal/util"
|
||||
|
|
|
@ -524,36 +524,36 @@ func (c *Config) requireCustomTLSForMySQL() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (c *Config) doBackup() error {
|
||||
func (c *Config) doBackup() (string, error) {
|
||||
now := time.Now().UTC()
|
||||
outputFile := filepath.Join(c.BackupsPath, fmt.Sprintf("backup_%s_%d.json", now.Weekday(), now.Hour()))
|
||||
providerLog(logger.LevelDebug, "starting backup to file %q", outputFile)
|
||||
err := os.MkdirAll(filepath.Dir(outputFile), 0700)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "unable to create backup dir %q: %v", outputFile, err)
|
||||
return fmt.Errorf("unable to create backup dir: %w", err)
|
||||
return outputFile, fmt.Errorf("unable to create backup dir: %w", err)
|
||||
}
|
||||
backup, err := DumpData()
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "unable to execute backup: %v", err)
|
||||
return fmt.Errorf("unable to dump backup data: %w", err)
|
||||
return outputFile, fmt.Errorf("unable to dump backup data: %w", err)
|
||||
}
|
||||
dump, err := json.Marshal(backup)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "unable to marshal backup as JSON: %v", err)
|
||||
return fmt.Errorf("unable to marshal backup data as JSON: %w", err)
|
||||
return outputFile, fmt.Errorf("unable to marshal backup data as JSON: %w", err)
|
||||
}
|
||||
err = os.WriteFile(outputFile, dump, 0600)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "unable to save backup: %v", err)
|
||||
return fmt.Errorf("unable to save backup: %w", err)
|
||||
return outputFile, fmt.Errorf("unable to save backup: %w", err)
|
||||
}
|
||||
providerLog(logger.LevelDebug, "backup saved to %q", outputFile)
|
||||
return nil
|
||||
return outputFile, nil
|
||||
}
|
||||
|
||||
// ExecuteBackup executes a backup
|
||||
func ExecuteBackup() error {
|
||||
func ExecuteBackup() (string, error) {
|
||||
return config.doBackup()
|
||||
}
|
||||
|
||||
|
@ -833,6 +833,12 @@ func Initialize(cnf Config, basePath string, checkAdmins bool) error {
|
|||
if cnf.BackupsPath == "" {
|
||||
return fmt.Errorf("required directory is invalid, backup path %#v", cnf.BackupsPath)
|
||||
}
|
||||
absoluteBackupPath, err := util.GetAbsolutePath(cnf.BackupsPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get absolute backup path: %w", err)
|
||||
}
|
||||
config.BackupsPath = absoluteBackupPath
|
||||
providerLog(logger.LevelDebug, "absolute backup path %q", config.BackupsPath)
|
||||
|
||||
if err := initializeHashingAlgo(&cnf); err != nil {
|
||||
return err
|
||||
|
@ -1866,10 +1872,6 @@ func GetUserVariants(username string) (User, User, error) {
|
|||
|
||||
// AddUser adds a new SFTPGo user.
|
||||
func AddUser(user *User, executor, ipAddress string) error {
|
||||
user.Filters.RecoveryCodes = nil
|
||||
user.Filters.TOTPConfig = UserTOTPConfig{
|
||||
Enabled: false,
|
||||
}
|
||||
user.Username = config.convertName(user.Username)
|
||||
err := provider.addUser(user)
|
||||
if err == nil {
|
||||
|
|
|
@ -1354,6 +1354,12 @@ func (r *EventRule) hasUserAssociated(providerObjectType string) bool {
|
|||
return providerObjectType == actionObjectUser
|
||||
case EventTriggerFsEvent:
|
||||
return true
|
||||
default:
|
||||
if len(r.Actions) > 0 {
|
||||
// should we allow schedules where backup is not the first action?
|
||||
// maybe we could pass the action index and check before that index
|
||||
return r.Actions[0].Type == ActionTypeBackup
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -2423,8 +2423,11 @@ func (p *MemoryProvider) getNextRuleID() int64 {
|
|||
func (p *MemoryProvider) clear() {
|
||||
p.dbHandle.Lock()
|
||||
defer p.dbHandle.Unlock()
|
||||
|
||||
p.dbHandle.usernames = []string{}
|
||||
p.dbHandle.users = make(map[string]User)
|
||||
p.dbHandle.groupnames = []string{}
|
||||
p.dbHandle.groups = map[string]Group{}
|
||||
p.dbHandle.vfoldersNames = []string{}
|
||||
p.dbHandle.vfolders = make(map[string]vfs.BaseVirtualFolder)
|
||||
p.dbHandle.admins = make(map[string]Admin)
|
||||
|
@ -2433,6 +2436,10 @@ func (p *MemoryProvider) clear() {
|
|||
p.dbHandle.apiKeysIDs = []string{}
|
||||
p.dbHandle.shares = make(map[string]Share)
|
||||
p.dbHandle.sharesIDs = []string{}
|
||||
p.dbHandle.actions = map[string]BaseEventAction{}
|
||||
p.dbHandle.actionsNames = []string{}
|
||||
p.dbHandle.rules = map[string]EventRule{}
|
||||
p.dbHandle.rulesNames = []string{}
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) reloadConfig() error {
|
||||
|
|
|
@ -25,8 +25,8 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/lestrrat-go/jwx/jwa"
|
||||
"github.com/lestrrat-go/jwx/jwt"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||
"github.com/rs/xid"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/httpclient"
|
||||
|
@ -128,12 +128,9 @@ func (n *Node) authenticate(token string) (string, error) {
|
|||
if token == "" {
|
||||
return "", ErrInvalidCredentials
|
||||
}
|
||||
t, err := jwt.Parse([]byte(token), jwt.WithVerify(jwa.HS256, []byte(n.Data.Key.GetPayload())))
|
||||
t, err := jwt.Parse([]byte(token), jwt.WithKey(jwa.HS256, []byte(n.Data.Key.GetPayload())), jwt.WithValidate(true))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to parse token: %v", err)
|
||||
}
|
||||
if err := jwt.Validate(t); err != nil {
|
||||
return "", fmt.Errorf("unable to validate token: %v", err)
|
||||
return "", fmt.Errorf("unable to parse and validate token: %v", err)
|
||||
}
|
||||
if admin, ok := t.Get("admin"); ok {
|
||||
if val, ok := admin.(string); ok && val != "" {
|
||||
|
@ -169,7 +166,7 @@ func (n *Node) generateAuthToken(username string) (string, error) {
|
|||
t.Set(jwt.NotBeforeKey, now.Add(-30*time.Second)) //nolint:errcheck
|
||||
t.Set(jwt.ExpirationKey, now.Add(1*time.Minute)) //nolint:errcheck
|
||||
|
||||
payload, err := jwt.Sign(t, jwa.HS256, []byte(n.Data.Key.GetPayload()))
|
||||
payload, err := jwt.Sign(t, jwt.WithKey(jwa.HS256, []byte(n.Data.Key.GetPayload())))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to sign authentication token: %w", err)
|
||||
}
|
||||
|
|
|
@ -987,6 +987,12 @@ func sqlCommonAddUser(user *User, dbHandle *sql.DB) error {
|
|||
defer cancel()
|
||||
|
||||
return sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {
|
||||
if config.IsShared == 1 {
|
||||
_, err := tx.ExecContext(ctx, getRemoveSoftDeletedUserQuery(), user.Username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
q := getAddUserQuery()
|
||||
_, err := tx.ExecContext(ctx, q, user.Username, user.Password, string(publicKeys), user.HomeDir, user.UID, user.GID,
|
||||
user.MaxSessions, user.QuotaSize, user.QuotaFiles, string(permissions), user.UploadBandwidth,
|
||||
|
@ -3127,6 +3133,12 @@ func sqlCommonAddEventRule(rule *EventRule, dbHandle *sql.DB) error {
|
|||
defer cancel()
|
||||
|
||||
return sqlCommonExecuteTx(ctx, dbHandle, func(tx *sql.Tx) error {
|
||||
if config.IsShared == 1 {
|
||||
_, err := tx.ExecContext(ctx, getRemoveSoftDeletedRuleQuery(), rule.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
q := getAddEventRuleQuery()
|
||||
_, err := tx.ExecContext(ctx, q, rule.Name, rule.Description, util.GetTimeAsMsSinceEpoch(time.Now()),
|
||||
util.GetTimeAsMsSinceEpoch(time.Now()), rule.Trigger, string(conditions))
|
||||
|
|
|
@ -527,6 +527,10 @@ func getDeleteUserQuery(softDelete bool) string {
|
|||
return fmt.Sprintf(`DELETE FROM %s WHERE id = %s`, sqlTableUsers, sqlPlaceholders[0])
|
||||
}
|
||||
|
||||
func getRemoveSoftDeletedUserQuery() string {
|
||||
return fmt.Sprintf(`DELETE FROM %s WHERE username = %s AND deleted_at > 0`, sqlTableUsers, sqlPlaceholders[0])
|
||||
}
|
||||
|
||||
func getFolderByNameQuery() string {
|
||||
return fmt.Sprintf(`SELECT %s FROM %s WHERE name = %s`, selectFolderFields, sqlTableFolders, sqlPlaceholders[0])
|
||||
}
|
||||
|
@ -891,6 +895,10 @@ func getDeleteEventRuleQuery(softDelete bool) string {
|
|||
return fmt.Sprintf(`DELETE FROM %s WHERE name = %s`, sqlTableEventsRules, sqlPlaceholders[0])
|
||||
}
|
||||
|
||||
func getRemoveSoftDeletedRuleQuery() string {
|
||||
return fmt.Sprintf(`DELETE FROM %s WHERE name = %s AND deleted_at > 0`, sqlTableEventsRules, sqlPlaceholders[0])
|
||||
}
|
||||
|
||||
func getClearRuleActionMappingQuery() string {
|
||||
return fmt.Sprintf(`DELETE FROM %s WHERE rule_id = (SELECT id FROM %s WHERE name = %s)`, sqlTableRulesActionsMapping,
|
||||
sqlTableEventsRules, sqlPlaceholders[0])
|
||||
|
|
|
@ -15,8 +15,6 @@
|
|||
package dataprovider
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
@ -24,9 +22,11 @@ import (
|
|||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/xid"
|
||||
"github.com/sftpgo/sdk"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/kms"
|
||||
|
@ -600,7 +600,7 @@ func (u *User) GetVirtualFolderForPath(virtualPath string) (vfs.VirtualFolder, e
|
|||
// CheckMetadataConsistency checks the consistency between the metadata stored
|
||||
// in the configured metadata plugin and the filesystem
|
||||
func (u *User) CheckMetadataConsistency() error {
|
||||
fs, err := u.getRootFs("")
|
||||
fs, err := u.getRootFs(xid.New().String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -621,7 +621,7 @@ func (u *User) CheckMetadataConsistency() error {
|
|||
// ScanQuota scans the user home dir and virtual folders, included in its quota,
|
||||
// and returns the number of files and their size
|
||||
func (u *User) ScanQuota() (int, int64, error) {
|
||||
fs, err := u.getRootFs("")
|
||||
fs, err := u.getRootFs(xid.New().String())
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
@ -1131,12 +1131,9 @@ func (u *User) MustSetSecondFactorForProtocol(protocol string) bool {
|
|||
}
|
||||
|
||||
// GetSignature returns a signature for this admin.
|
||||
// It could change after an update
|
||||
// It will change after an update
|
||||
func (u *User) GetSignature() string {
|
||||
data := []byte(fmt.Sprintf("%v_%v_%v", u.Username, u.Status, u.ExpirationDate))
|
||||
data = append(data, []byte(u.Password)...)
|
||||
signature := sha256.Sum256(data)
|
||||
return base64.StdEncoding.EncodeToString(signature[:])
|
||||
return strconv.FormatInt(u.UpdatedAt, 10)
|
||||
}
|
||||
|
||||
// GetBandwidthForIP returns the upload and download bandwidth for the specified IP
|
||||
|
|
|
@ -81,6 +81,10 @@ func addUser(w http.ResponseWriter, r *http.Request) {
|
|||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
user.Filters.RecoveryCodes = nil
|
||||
user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{
|
||||
Enabled: false,
|
||||
}
|
||||
err = dataprovider.AddUser(&user, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
|
|
|
@ -21,7 +21,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"github.com/lestrrat-go/jwx/jwt"
|
||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||
"github.com/rs/xid"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
||||
|
|
|
@ -32,7 +32,7 @@ import (
|
|||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"github.com/lestrrat-go/jwx/jwa"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/common"
|
||||
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
||||
|
|
|
@ -4516,7 +4516,7 @@ func TestUserSFTPFs(t *testing.T) {
|
|||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
user.FsConfig.Provider = sdk.SFTPFilesystemProvider
|
||||
user.FsConfig.SFTPConfig.Endpoint = "127.0.0.1" // missing port
|
||||
user.FsConfig.SFTPConfig.Endpoint = "[::1]:22:22" // invalid endpoint
|
||||
user.FsConfig.SFTPConfig.Username = "sftp_user"
|
||||
user.FsConfig.SFTPConfig.Password = kms.NewPlainSecret("sftp_pwd")
|
||||
user.FsConfig.SFTPConfig.PrivateKey = kms.NewPlainSecret(sftpPrivateKey)
|
||||
|
@ -4527,6 +4527,13 @@ func TestUserSFTPFs(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(resp), "invalid endpoint")
|
||||
|
||||
user.FsConfig.SFTPConfig.Endpoint = "127.0.0.1"
|
||||
_, _, err = httpdtest.UpdateUser(user, http.StatusBadRequest, "")
|
||||
assert.Error(t, err)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "127.0.0.1:22", user.FsConfig.SFTPConfig.Endpoint)
|
||||
|
||||
user.FsConfig.SFTPConfig.Endpoint = "127.0.0.1:2022"
|
||||
user.FsConfig.SFTPConfig.DisableCouncurrentReads = true
|
||||
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
|
|
|
@ -40,8 +40,8 @@ import (
|
|||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"github.com/klauspost/compress/zip"
|
||||
"github.com/lestrrat-go/jwx/jwa"
|
||||
"github.com/lestrrat-go/jwx/jwt"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||
"github.com/rs/xid"
|
||||
"github.com/sftpgo/sdk"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
|
@ -21,7 +21,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"github.com/lestrrat-go/jwx/jwt"
|
||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||
"github.com/rs/xid"
|
||||
"github.com/sftpgo/sdk"
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ import (
|
|||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"github.com/lestrrat-go/jwx/jwa"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
"github.com/rs/xid"
|
||||
"github.com/sftpgo/sdk"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
|
@ -31,7 +31,7 @@ import (
|
|||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/lestrrat-go/jwx/jwa"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
"github.com/rs/cors"
|
||||
"github.com/rs/xid"
|
||||
"github.com/sftpgo/sdk"
|
||||
|
|
|
@ -1366,14 +1366,14 @@ func getSecretFromFormField(r *http.Request, field string) *kms.Secret {
|
|||
func getS3Config(r *http.Request) (vfs.S3FsConfig, error) {
|
||||
var err error
|
||||
config := vfs.S3FsConfig{}
|
||||
config.Bucket = r.Form.Get("s3_bucket")
|
||||
config.Region = r.Form.Get("s3_region")
|
||||
config.AccessKey = r.Form.Get("s3_access_key")
|
||||
config.RoleARN = r.Form.Get("s3_role_arn")
|
||||
config.Bucket = strings.TrimSpace(r.Form.Get("s3_bucket"))
|
||||
config.Region = strings.TrimSpace(r.Form.Get("s3_region"))
|
||||
config.AccessKey = strings.TrimSpace(r.Form.Get("s3_access_key"))
|
||||
config.RoleARN = strings.TrimSpace(r.Form.Get("s3_role_arn"))
|
||||
config.AccessSecret = getSecretFromFormField(r, "s3_access_secret")
|
||||
config.Endpoint = r.Form.Get("s3_endpoint")
|
||||
config.StorageClass = r.Form.Get("s3_storage_class")
|
||||
config.ACL = r.Form.Get("s3_acl")
|
||||
config.Endpoint = strings.TrimSpace(r.Form.Get("s3_endpoint"))
|
||||
config.StorageClass = strings.TrimSpace(r.Form.Get("s3_storage_class"))
|
||||
config.ACL = strings.TrimSpace(r.Form.Get("s3_acl"))
|
||||
config.KeyPrefix = r.Form.Get("s3_key_prefix")
|
||||
config.UploadPartSize, err = strconv.ParseInt(r.Form.Get("s3_upload_part_size"), 10, 64)
|
||||
if err != nil {
|
||||
|
@ -1407,9 +1407,9 @@ func getGCSConfig(r *http.Request) (vfs.GCSFsConfig, error) {
|
|||
var err error
|
||||
config := vfs.GCSFsConfig{}
|
||||
|
||||
config.Bucket = r.Form.Get("gcs_bucket")
|
||||
config.StorageClass = r.Form.Get("gcs_storage_class")
|
||||
config.ACL = r.Form.Get("gcs_acl")
|
||||
config.Bucket = strings.TrimSpace(r.Form.Get("gcs_bucket"))
|
||||
config.StorageClass = strings.TrimSpace(r.Form.Get("gcs_storage_class"))
|
||||
config.ACL = strings.TrimSpace(r.Form.Get("gcs_acl"))
|
||||
config.KeyPrefix = r.Form.Get("gcs_key_prefix")
|
||||
autoCredentials := r.Form.Get("gcs_auto_credentials")
|
||||
if autoCredentials != "" {
|
||||
|
@ -1440,7 +1440,7 @@ func getGCSConfig(r *http.Request) (vfs.GCSFsConfig, error) {
|
|||
func getSFTPConfig(r *http.Request) (vfs.SFTPFsConfig, error) {
|
||||
var err error
|
||||
config := vfs.SFTPFsConfig{}
|
||||
config.Endpoint = r.Form.Get("sftp_endpoint")
|
||||
config.Endpoint = strings.TrimSpace(r.Form.Get("sftp_endpoint"))
|
||||
config.Username = r.Form.Get("sftp_username")
|
||||
config.Password = getSecretFromFormField(r, "sftp_password")
|
||||
config.PrivateKey = getSecretFromFormField(r, "sftp_private_key")
|
||||
|
@ -1463,7 +1463,7 @@ func getSFTPConfig(r *http.Request) (vfs.SFTPFsConfig, error) {
|
|||
|
||||
func getHTTPFsConfig(r *http.Request) vfs.HTTPFsConfig {
|
||||
config := vfs.HTTPFsConfig{}
|
||||
config.Endpoint = r.Form.Get("http_endpoint")
|
||||
config.Endpoint = strings.TrimSpace(r.Form.Get("http_endpoint"))
|
||||
config.Username = r.Form.Get("http_username")
|
||||
config.SkipTLSVerify = r.Form.Get("http_skip_tls_verify") != ""
|
||||
config.Password = getSecretFromFormField(r, "http_password")
|
||||
|
@ -1479,13 +1479,13 @@ func getHTTPFsConfig(r *http.Request) vfs.HTTPFsConfig {
|
|||
func getAzureConfig(r *http.Request) (vfs.AzBlobFsConfig, error) {
|
||||
var err error
|
||||
config := vfs.AzBlobFsConfig{}
|
||||
config.Container = r.Form.Get("az_container")
|
||||
config.AccountName = r.Form.Get("az_account_name")
|
||||
config.Container = strings.TrimSpace(r.Form.Get("az_container"))
|
||||
config.AccountName = strings.TrimSpace(r.Form.Get("az_account_name"))
|
||||
config.AccountKey = getSecretFromFormField(r, "az_account_key")
|
||||
config.SASURL = getSecretFromFormField(r, "az_sas_url")
|
||||
config.Endpoint = r.Form.Get("az_endpoint")
|
||||
config.Endpoint = strings.TrimSpace(r.Form.Get("az_endpoint"))
|
||||
config.KeyPrefix = r.Form.Get("az_key_prefix")
|
||||
config.AccessTier = r.Form.Get("az_access_tier")
|
||||
config.AccessTier = strings.TrimSpace(r.Form.Get("az_access_tier"))
|
||||
config.UseEmulator = r.Form.Get("az_use_emulator") != ""
|
||||
config.UploadPartSize, err = strconv.ParseInt(r.Form.Get("az_upload_part_size"), 10, 64)
|
||||
if err != nil {
|
||||
|
@ -2773,6 +2773,10 @@ func (s *httpdServer) handleWebAddUserPost(w http.ResponseWriter, r *http.Reques
|
|||
Password: user.Password,
|
||||
PublicKeys: user.PublicKeys,
|
||||
})
|
||||
user.Filters.RecoveryCodes = nil
|
||||
user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{
|
||||
Enabled: false,
|
||||
}
|
||||
err = dataprovider.AddUser(&user, claims.Username, ipAddr)
|
||||
if err != nil {
|
||||
s.renderUserPage(w, r, &user, userPageModeAdd, err.Error())
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
|
@ -113,10 +112,9 @@ func (p *authPlugin) initialize() error {
|
|||
return fmt.Errorf("invalid options for auth plugin %#v: %v", p.config.Cmd, err)
|
||||
}
|
||||
|
||||
var secureConfig *plugin.SecureConfig
|
||||
if p.config.SHA256Sum != "" {
|
||||
secureConfig.Checksum = []byte(p.config.SHA256Sum)
|
||||
secureConfig.Hash = sha256.New()
|
||||
secureConfig, err := p.config.getSecureConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := plugin.NewClient(&plugin.ClientConfig{
|
||||
HandshakeConfig: auth.Handshake,
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
|
||||
|
@ -54,10 +53,9 @@ func (p *ipFilterPlugin) cleanup() {
|
|||
func (p *ipFilterPlugin) initialize() error {
|
||||
logger.Debug(logSender, "", "create new IP filter plugin %#v", p.config.Cmd)
|
||||
killProcess(p.config.Cmd)
|
||||
var secureConfig *plugin.SecureConfig
|
||||
if p.config.SHA256Sum != "" {
|
||||
secureConfig.Checksum = []byte(p.config.SHA256Sum)
|
||||
secureConfig.Hash = sha256.New()
|
||||
secureConfig, err := p.config.getSecureConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := plugin.NewClient(&plugin.ClientConfig{
|
||||
HandshakeConfig: ipfilter.Handshake,
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
@ -75,10 +74,9 @@ func (p *kmsPlugin) initialize() error {
|
|||
if err := p.config.KMSOptions.validate(); err != nil {
|
||||
return fmt.Errorf("invalid options for kms plugin %#v: %v", p.config.Cmd, err)
|
||||
}
|
||||
var secureConfig *plugin.SecureConfig
|
||||
if p.config.SHA256Sum != "" {
|
||||
secureConfig.Checksum = []byte(p.config.SHA256Sum)
|
||||
secureConfig.Hash = sha256.New()
|
||||
secureConfig, err := p.config.getSecureConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := plugin.NewClient(&plugin.ClientConfig{
|
||||
HandshakeConfig: kmsplugin.Handshake,
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
|
||||
|
@ -54,10 +53,9 @@ func (p *metadataPlugin) cleanup() {
|
|||
func (p *metadataPlugin) initialize() error {
|
||||
killProcess(p.config.Cmd)
|
||||
logger.Debug(logSender, "", "create new metadata plugin %#v", p.config.Cmd)
|
||||
var secureConfig *plugin.SecureConfig
|
||||
if p.config.SHA256Sum != "" {
|
||||
secureConfig.Checksum = []byte(p.config.SHA256Sum)
|
||||
secureConfig.Hash = sha256.New()
|
||||
secureConfig, err := p.config.getSecureConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := plugin.NewClient(&plugin.ClientConfig{
|
||||
HandshakeConfig: metadata.Handshake,
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"sync"
|
||||
|
@ -138,10 +137,9 @@ func (p *notifierPlugin) initialize() error {
|
|||
if !p.config.NotifierOptions.hasActions() {
|
||||
return fmt.Errorf("no actions defined for the notifier plugin %#v", p.config.Cmd)
|
||||
}
|
||||
var secureConfig *plugin.SecureConfig
|
||||
if p.config.SHA256Sum != "" {
|
||||
secureConfig.Checksum = []byte(p.config.SHA256Sum)
|
||||
secureConfig.Hash = sha256.New()
|
||||
secureConfig, err := p.config.getSecureConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := plugin.NewClient(&plugin.ClientConfig{
|
||||
HandshakeConfig: notifier.Handshake,
|
||||
|
|
|
@ -16,7 +16,9 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
@ -24,6 +26,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/go-plugin"
|
||||
"github.com/sftpgo/sdk/plugin/auth"
|
||||
"github.com/sftpgo/sdk/plugin/eventsearcher"
|
||||
"github.com/sftpgo/sdk/plugin/ipfilter"
|
||||
|
@ -82,6 +85,20 @@ type Config struct {
|
|||
kmsID int
|
||||
}
|
||||
|
||||
func (c *Config) getSecureConfig() (*plugin.SecureConfig, error) {
|
||||
if c.SHA256Sum != "" {
|
||||
checksum, err := hex.DecodeString(c.SHA256Sum)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid sha256 hash %q: %w", c.SHA256Sum, err)
|
||||
}
|
||||
return &plugin.SecureConfig{
|
||||
Checksum: checksum,
|
||||
Hash: sha256.New(),
|
||||
}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *Config) newKMSPluginSecretProvider(base kms.BaseSecret, url, masterKey string) kms.SecretProvider {
|
||||
return &kmsPluginSecretProvider{
|
||||
BaseSecret: base,
|
||||
|
@ -774,16 +791,17 @@ func setLogLevel(logLevel string) {
|
|||
|
||||
func startCheckTicker() {
|
||||
logger.Debug(logSender, "", "start plugins checker")
|
||||
checker := time.NewTicker(30 * time.Second)
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-Handler.done:
|
||||
logger.Debug(logSender, "", "handler done, stop plugins checker")
|
||||
checker.Stop()
|
||||
return
|
||||
case <-checker.C:
|
||||
case <-ticker.C:
|
||||
Handler.checkCrashedPlugins()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
|
||||
|
@ -54,10 +53,9 @@ func (p *searcherPlugin) cleanup() {
|
|||
func (p *searcherPlugin) initialize() error {
|
||||
killProcess(p.config.Cmd)
|
||||
logger.Debug(logSender, "", "create new searcher plugin %#v", p.config.Cmd)
|
||||
var secureConfig *plugin.SecureConfig
|
||||
if p.config.SHA256Sum != "" {
|
||||
secureConfig.Checksum = []byte(p.config.SHA256Sum)
|
||||
secureConfig.Hash = sha256.New()
|
||||
secureConfig, err := p.config.getSecureConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := plugin.NewClient(&plugin.ClientConfig{
|
||||
HandshakeConfig: eventsearcher.Handshake,
|
||||
|
|
|
@ -182,9 +182,6 @@ func (c *Connection) handleFilewrite(request *sftp.Request) (sftp.WriterAtReader
|
|||
func (c *Connection) Filecmd(request *sftp.Request) error {
|
||||
c.UpdateLastActivity()
|
||||
|
||||
c.Log(logger.LevelDebug, "new cmd, method: %v, sourcePath: %#v, targetPath: %#v", request.Method,
|
||||
request.Filepath, request.Target)
|
||||
|
||||
switch request.Method {
|
||||
case "Setstat":
|
||||
return c.handleSFTPSetstat(request)
|
||||
|
|
|
@ -1926,6 +1926,30 @@ func TestSupportedSecurityOptions(t *testing.T) {
|
|||
assert.Equal(t, supportedKexAlgos, serverConfig.KeyExchanges)
|
||||
}
|
||||
|
||||
func TestLoadModuli(t *testing.T) {
|
||||
dhGEXSha1 := "diffie-hellman-group-exchange-sha1"
|
||||
dhGEXSha256 := "diffie-hellman-group-exchange-sha256"
|
||||
c := Configuration{}
|
||||
c.Moduli = []string{".", "missing file"}
|
||||
err := c.loadModuli(configDir)
|
||||
assert.Error(t, err)
|
||||
assert.NotContains(t, supportedKexAlgos, dhGEXSha1)
|
||||
assert.NotContains(t, supportedKexAlgos, dhGEXSha256)
|
||||
assert.Len(t, supportedKexAlgos, 10)
|
||||
moduli := []byte("20220414072358 2 6 100 2047 5 F19C2D09AD49978F8A0C1B84168A4011A26F9CD516815934764A319FDC5975FA514AAF11B747D8CA6B3919532BEFB68FA118079473895674F3770F71FBB742F176883841EB3DE679BEF53C6AFE437A662F228B03C1E34B5A0D3909F608CEAA16C1F8131DE11E67878EFD918A89205E5E4DE323054010CA4711F25D466BB7727A016DD3F9F53BDBCE093055A4F2497ADEFB5A2500F9C5C3B0BCD88C6489F4C1CBC7CFB67BA6EABA0195794E4188CE9060F431041AD52FB9BAC4DF7FA536F585FBE67746CD57BFAD67567E9706C24D95C49BE95B759657C6BB5151E2AEA32F4CD557C40298A5C402101520EE8AAB8DFEED6FFC11AAF8036D6345923CFB5D1B922F")
|
||||
moduliFile := filepath.Join(os.TempDir(), "moduli")
|
||||
err = os.WriteFile(moduliFile, moduli, 0600)
|
||||
assert.NoError(t, err)
|
||||
c.Moduli = []string{moduliFile}
|
||||
err = c.loadModuli(configDir)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, supportedKexAlgos, dhGEXSha1)
|
||||
assert.Contains(t, supportedKexAlgos, dhGEXSha256)
|
||||
assert.Len(t, supportedKexAlgos, 12)
|
||||
err = os.Remove(moduliFile)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLoadHostKeys(t *testing.T) {
|
||||
serverConfig := &ssh.ServerConfig{}
|
||||
c := Configuration{}
|
||||
|
|
|
@ -141,6 +141,12 @@ type Configuration struct {
|
|||
// HostKeyAlgorithms lists the public key algorithms that the server will accept for host
|
||||
// key authentication.
|
||||
HostKeyAlgorithms []string `json:"host_key_algorithms" mapstructure:"host_key_algorithms"`
|
||||
// Diffie-Hellman moduli files.
|
||||
// Each moduli file can be defined as a path relative to the configuration directory or an absolute one.
|
||||
// If set, "diffie-hellman-group-exchange-sha256" and "diffie-hellman-group-exchange-sha1" KEX algorithms
|
||||
// will be available, `diffie-hellman-group-exchange-sha256` will be enabled by default if you
|
||||
// don't explicitly set KEXs
|
||||
Moduli []string `json:"moduli" mapstructure:"moduli"`
|
||||
// KexAlgorithms specifies the available KEX (Key Exchange) algorithms in
|
||||
// preference order.
|
||||
KexAlgorithms []string `json:"kex_algorithms" mapstructure:"kex_algorithms"`
|
||||
|
@ -294,6 +300,10 @@ func (c *Configuration) Initialize(configDir string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if err := c.loadModuli(configDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sftp.SetSFTPExtensions(sftpExtensions...) //nolint:errcheck // we configure valid SFTP Extensions so we cannot get an error
|
||||
|
||||
if err := c.configureSecurityOptions(serverConfig); err != nil {
|
||||
|
@ -527,14 +537,12 @@ func (c *Configuration) AcceptInboundConnection(conn net.Conn, config *ssh.Serve
|
|||
loginType := sconn.Permissions.Extensions["sftpgo_login_method"]
|
||||
connectionID := hex.EncodeToString(sconn.SessionID())
|
||||
|
||||
defer user.CloseFs() //nolint:errcheck
|
||||
if err = user.CheckFsRoot(connectionID); err != nil {
|
||||
errClose := user.CloseFs()
|
||||
logger.Warn(logSender, connectionID, "unable to check fs root: %v close fs error: %v", err, errClose)
|
||||
logger.Warn(logSender, connectionID, "unable to check fs root: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
defer user.CloseFs() //nolint:errcheck
|
||||
|
||||
logger.Log(logger.LevelInfo, common.ProtocolSSH, connectionID,
|
||||
"User %#v logged in with %#v, from ip %#v, client version %#v", user.Username, loginType,
|
||||
ipAddr, string(sconn.ClientVersion()))
|
||||
|
@ -842,6 +850,29 @@ func (c *Configuration) checkHostKeyAutoGeneration(configDir string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *Configuration) loadModuli(configDir string) error {
|
||||
supportedKexAlgos = util.Remove(supportedKexAlgos, "diffie-hellman-group-exchange-sha1")
|
||||
supportedKexAlgos = util.Remove(supportedKexAlgos, "diffie-hellman-group-exchange-sha256")
|
||||
for _, m := range c.Moduli {
|
||||
m = strings.TrimSpace(m)
|
||||
if !util.IsFileInputValid(m) {
|
||||
logger.Warn(logSender, "", "unable to load invalid moduli file %q", m)
|
||||
logger.WarnToConsole("unable to load invalid host moduli file %q", m)
|
||||
continue
|
||||
}
|
||||
if !filepath.IsAbs(m) {
|
||||
m = filepath.Join(configDir, m)
|
||||
}
|
||||
logger.Info(logSender, "", "loading moduli file %q", m)
|
||||
if err := ssh.ParseModuli(m); err != nil {
|
||||
return err
|
||||
}
|
||||
supportedKexAlgos = append(supportedKexAlgos, "diffie-hellman-group-exchange-sha1",
|
||||
"diffie-hellman-group-exchange-sha256")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// If no host keys are defined we try to use or generate the default ones.
|
||||
func (c *Configuration) checkAndLoadHostKeys(configDir string, serverConfig *ssh.ServerConfig) error {
|
||||
if err := c.checkHostKeyAutoGeneration(configDir); err != nil {
|
||||
|
|
|
@ -182,7 +182,7 @@ func TestMain(m *testing.M) {
|
|||
logFilePath = filepath.Join(configDir, "sftpgo_sftpd_test.log")
|
||||
loginBannerFileName := "login_banner"
|
||||
loginBannerFile := filepath.Join(configDir, loginBannerFileName)
|
||||
logger.InitLogger(logFilePath, 5, 1, 28, false, false, zerolog.DebugLevel)
|
||||
logger.InitLogger(logFilePath, 10, 1, 28, false, false, zerolog.DebugLevel)
|
||||
err := os.WriteFile(loginBannerFile, []byte("simple login banner\n"), os.ModePerm)
|
||||
if err != nil {
|
||||
logger.ErrorToConsole("error creating login banner: %v", err)
|
||||
|
@ -401,6 +401,12 @@ func TestInitialization(t *testing.T) {
|
|||
assert.True(t, sftpdConf.Bindings[0].HasProxy())
|
||||
err = sftpdConf.Initialize(configDir)
|
||||
assert.Error(t, err)
|
||||
sftpdConf.Moduli = []string{"missing moduli file"}
|
||||
err = sftpdConf.Initialize(configDir)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "unable to open moduli file")
|
||||
}
|
||||
sftpdConf.Moduli = nil
|
||||
sftpdConf.HostKeys = []string{"missing key"}
|
||||
err = sftpdConf.Initialize(configDir)
|
||||
assert.Error(t, err)
|
||||
|
@ -5279,9 +5285,7 @@ func TestSFTPLoopVirtualFolders(t *testing.T) {
|
|||
defer client.Close()
|
||||
assert.NoError(t, checkBasicSFTP(client))
|
||||
_, err = client.ReadDir("/vdir")
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "SFTP loop")
|
||||
}
|
||||
assert.Error(t, err)
|
||||
}
|
||||
// now make user2 a local account with an SFTP virtual folder to user1.
|
||||
// So we have:
|
||||
|
@ -5316,9 +5320,7 @@ func TestSFTPLoopVirtualFolders(t *testing.T) {
|
|||
defer client.Close()
|
||||
assert.NoError(t, checkBasicSFTP(client))
|
||||
_, err = client.ReadDir("/vdir")
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "SFTP loop")
|
||||
}
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
_, err = httpdtest.RemoveUser(user1, http.StatusOK)
|
||||
|
@ -10080,7 +10082,7 @@ func TestSCPNestedFolders(t *testing.T) {
|
|||
|
||||
// now change the password for the base user, so SFTP folder will not work
|
||||
baseUser.Password = defaultPassword + "_mod"
|
||||
_, _, err = httpdtest.UpdateUser(baseUser, http.StatusOK, "")
|
||||
_, _, err = httpdtest.UpdateUser(baseUser, http.StatusOK, "1")
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = scpUpload(filepath.Join(baseDir, "vdir"), remoteRootPath, true, false)
|
||||
|
|
|
@ -181,6 +181,7 @@ func SendEmail(to []string, subject, body string, contentType EmailContentType,
|
|||
}
|
||||
|
||||
email := mail.NewMSG()
|
||||
email.AllowDuplicateAddress = true
|
||||
if from != "" {
|
||||
email.SetFrom(from)
|
||||
} else {
|
||||
|
|
|
@ -125,6 +125,18 @@ func Contains[T comparable](elems []T, v T) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// Remove removes an element from a string slice and
|
||||
// returns the modified slice
|
||||
func Remove(elems []string, val string) []string {
|
||||
for idx, v := range elems {
|
||||
if v == val {
|
||||
elems[idx] = elems[len(elems)-1]
|
||||
return elems[:len(elems)-1]
|
||||
}
|
||||
}
|
||||
return elems
|
||||
}
|
||||
|
||||
// IsStringPrefixInSlice searches a string prefix in a slice and returns true
|
||||
// if a matching prefix is found
|
||||
func IsStringPrefixInSlice(obj string, list []string) bool {
|
||||
|
@ -727,3 +739,19 @@ func PanicOnError(err error) {
|
|||
panic(fmt.Errorf("unexpected error: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
// GetAbsolutePath returns an absolute path using the current dir as base
|
||||
// if name defines a relative path
|
||||
func GetAbsolutePath(name string) (string, error) {
|
||||
if name == "" {
|
||||
return name, errors.New("input path cannot be empty")
|
||||
}
|
||||
if filepath.IsAbs(name) {
|
||||
return name, nil
|
||||
}
|
||||
curDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return name, err
|
||||
}
|
||||
return filepath.Join(curDir, name), nil
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ package version
|
|||
|
||||
import "strings"
|
||||
|
||||
const version = "2.4.0-dev"
|
||||
const version = "2.4.2"
|
||||
|
||||
var (
|
||||
commit = ""
|
||||
|
|
|
@ -55,6 +55,7 @@ import (
|
|||
const (
|
||||
azureDefaultEndpoint = "blob.core.windows.net"
|
||||
azBlobFsName = "AzureBlobFs"
|
||||
azFolderKey = "hdi_isfolder"
|
||||
)
|
||||
|
||||
// AzureBlobFs is a Fs implementation for Azure Blob storage.
|
||||
|
@ -182,6 +183,14 @@ func (fs *AzureBlobFs) Stat(name string) (os.FileInfo, error) {
|
|||
if err == nil {
|
||||
contentType := util.GetStringFromPointer(attrs.ContentType)
|
||||
isDir := contentType == dirMimeType
|
||||
if !isDir {
|
||||
for k, v := range attrs.Metadata {
|
||||
if strings.ToLower(k) == azFolderKey {
|
||||
isDir = (v == "true")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
metric.AZListObjectsCompleted(nil)
|
||||
return updateFileInfoModTime(fs.getStorageID(), name, NewFileInfo(name, isDir,
|
||||
util.GetIntFromPointer(attrs.ContentLength),
|
||||
|
@ -217,7 +226,7 @@ func (fs *AzureBlobFs) Open(name string, offset int64) (File, *pipeat.PipeReader
|
|||
go func() {
|
||||
defer cancelFn()
|
||||
|
||||
blockBlob := fs.containerClient.NewBlockBlobClient(name)
|
||||
blockBlob := fs.containerClient.NewBlockBlobClient(url.PathEscape(name))
|
||||
err := fs.handleMultipartDownload(ctx, blockBlob, offset, w)
|
||||
w.CloseWithError(err) //nolint:errcheck
|
||||
fsLog(fs, logger.LevelDebug, "download completed, path: %#v size: %v, err: %+v", name, w.GetWrittenBytes(), err)
|
||||
|
@ -238,8 +247,12 @@ func (fs *AzureBlobFs) Create(name string, flag int) (File, *PipeWriter, func(),
|
|||
p := NewPipeWriter(w)
|
||||
headers := blob.HTTPHeaders{}
|
||||
var contentType string
|
||||
var metadata map[string]string
|
||||
if flag == -1 {
|
||||
contentType = dirMimeType
|
||||
metadata = map[string]string{
|
||||
azFolderKey: "true",
|
||||
}
|
||||
} else {
|
||||
contentType = mime.TypeByExtension(path.Ext(name))
|
||||
}
|
||||
|
@ -250,8 +263,8 @@ func (fs *AzureBlobFs) Create(name string, flag int) (File, *PipeWriter, func(),
|
|||
go func() {
|
||||
defer cancelFn()
|
||||
|
||||
blockBlob := fs.containerClient.NewBlockBlobClient(name)
|
||||
err := fs.handleMultipartUpload(ctx, r, blockBlob, &headers)
|
||||
blockBlob := fs.containerClient.NewBlockBlobClient(url.PathEscape(name))
|
||||
err := fs.handleMultipartUpload(ctx, r, blockBlob, &headers, metadata)
|
||||
r.CloseWithError(err) //nolint:errcheck
|
||||
p.Done(err)
|
||||
fsLog(fs, logger.LevelDebug, "upload completed, path: %#v, readed bytes: %v, err: %+v", name, r.GetReadedBytes(), err)
|
||||
|
@ -282,43 +295,47 @@ func (fs *AzureBlobFs) Rename(source, target string) error {
|
|||
if hasContents {
|
||||
return fmt.Errorf("cannot rename non empty directory: %#v", source)
|
||||
}
|
||||
}
|
||||
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxLongTimeout))
|
||||
defer cancelFn()
|
||||
|
||||
srcBlob := fs.containerClient.NewBlockBlobClient(url.PathEscape(source))
|
||||
dstBlob := fs.containerClient.NewBlockBlobClient(target)
|
||||
resp, err := dstBlob.StartCopyFromURL(ctx, srcBlob.URL(), fs.getCopyOptions())
|
||||
if err != nil {
|
||||
metric.AZCopyObjectCompleted(err)
|
||||
return err
|
||||
}
|
||||
copyStatus := blob.CopyStatusType(util.GetStringFromPointer((*string)(resp.CopyStatus)))
|
||||
nErrors := 0
|
||||
for copyStatus == blob.CopyStatusTypePending {
|
||||
// Poll until the copy is complete.
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
resp, err := dstBlob.GetProperties(ctx, &blob.GetPropertiesOptions{})
|
||||
if err != nil {
|
||||
// A GetProperties failure may be transient, so allow a couple
|
||||
// of them before giving up.
|
||||
nErrors++
|
||||
if ctx.Err() != nil || nErrors == 3 {
|
||||
metric.AZCopyObjectCompleted(err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
copyStatus = blob.CopyStatusType(util.GetStringFromPointer((*string)(resp.CopyStatus)))
|
||||
if err := fs.mkdirInternal(target); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if copyStatus != blob.CopyStatusTypeSuccess {
|
||||
err := fmt.Errorf("copy failed with status: %s", copyStatus)
|
||||
metric.AZCopyObjectCompleted(err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxLongTimeout))
|
||||
defer cancelFn()
|
||||
|
||||
metric.AZCopyObjectCompleted(nil)
|
||||
fs.preserveModificationTime(source, target, fi)
|
||||
srcBlob := fs.containerClient.NewBlockBlobClient(url.PathEscape(source))
|
||||
dstBlob := fs.containerClient.NewBlockBlobClient(url.PathEscape(target))
|
||||
resp, err := dstBlob.StartCopyFromURL(ctx, srcBlob.URL(), fs.getCopyOptions())
|
||||
if err != nil {
|
||||
metric.AZCopyObjectCompleted(err)
|
||||
return err
|
||||
}
|
||||
copyStatus := blob.CopyStatusType(util.GetStringFromPointer((*string)(resp.CopyStatus)))
|
||||
nErrors := 0
|
||||
for copyStatus == blob.CopyStatusTypePending {
|
||||
// Poll until the copy is complete.
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
resp, err := dstBlob.GetProperties(ctx, &blob.GetPropertiesOptions{})
|
||||
if err != nil {
|
||||
// A GetProperties failure may be transient, so allow a couple
|
||||
// of them before giving up.
|
||||
nErrors++
|
||||
if ctx.Err() != nil || nErrors == 3 {
|
||||
metric.AZCopyObjectCompleted(err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
copyStatus = blob.CopyStatusType(util.GetStringFromPointer((*string)(resp.CopyStatus)))
|
||||
}
|
||||
}
|
||||
if copyStatus != blob.CopyStatusTypeSuccess {
|
||||
err := fmt.Errorf("copy failed with status: %s", copyStatus)
|
||||
metric.AZCopyObjectCompleted(err)
|
||||
return err
|
||||
}
|
||||
|
||||
metric.AZCopyObjectCompleted(nil)
|
||||
fs.preserveModificationTime(source, target, fi)
|
||||
}
|
||||
return fs.Remove(source, fi.IsDir())
|
||||
}
|
||||
|
||||
|
@ -337,11 +354,22 @@ func (fs *AzureBlobFs) Remove(name string, isDir bool) error {
|
|||
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
|
||||
defer cancelFn()
|
||||
|
||||
blobBlock := fs.containerClient.NewBlockBlobClient(name)
|
||||
deletSnapshots := blob.DeleteSnapshotsOptionTypeInclude
|
||||
blobBlock := fs.containerClient.NewBlockBlobClient(url.PathEscape(name))
|
||||
var deletSnapshots blob.DeleteSnapshotsOptionType
|
||||
if !isDir {
|
||||
deletSnapshots = blob.DeleteSnapshotsOptionTypeInclude
|
||||
}
|
||||
_, err := blobBlock.Delete(ctx, &blob.DeleteOptions{
|
||||
DeleteSnapshots: &deletSnapshots,
|
||||
})
|
||||
if err != nil && isDir {
|
||||
if fs.isBadRequestError(err) {
|
||||
deletSnapshots = blob.DeleteSnapshotsOptionTypeInclude
|
||||
_, err = blobBlock.Delete(ctx, &blob.DeleteOptions{
|
||||
DeleteSnapshots: &deletSnapshots,
|
||||
})
|
||||
}
|
||||
}
|
||||
metric.AZDeleteObjectCompleted(err)
|
||||
if plugin.Handler.HasMetadater() && err == nil && !isDir {
|
||||
if errMetadata := plugin.Handler.RemoveMetadata(fs.getStorageID(), ensureAbsPath(name)); errMetadata != nil {
|
||||
|
@ -357,11 +385,7 @@ func (fs *AzureBlobFs) Mkdir(name string) error {
|
|||
if !fs.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
_, w, _, err := fs.Create(name, -1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return w.Close()
|
||||
return fs.mkdirInternal(name)
|
||||
}
|
||||
|
||||
// Symlink creates source as a symbolic link to target.
|
||||
|
@ -424,8 +448,10 @@ func (fs *AzureBlobFs) ReadDir(dirname string) ([]os.FileInfo, error) {
|
|||
prefixes := make(map[string]bool)
|
||||
|
||||
pager := fs.containerClient.NewListBlobsHierarchyPager("/", &container.ListBlobsHierarchyOptions{
|
||||
Include: container.ListBlobsInclude{},
|
||||
Prefix: &prefix,
|
||||
Include: container.ListBlobsInclude{
|
||||
//Metadata: true,
|
||||
},
|
||||
Prefix: &prefix,
|
||||
})
|
||||
|
||||
for pager.More() {
|
||||
|
@ -462,7 +488,7 @@ func (fs *AzureBlobFs) ReadDir(dirname string) ([]os.FileInfo, error) {
|
|||
size = util.GetIntFromPointer(blobItem.Properties.ContentLength)
|
||||
modTime = util.GetTimeFromPointer(blobItem.Properties.LastModified)
|
||||
contentType := util.GetStringFromPointer(blobItem.Properties.ContentType)
|
||||
isDir = (contentType == dirMimeType)
|
||||
isDir = checkDirectoryMarkers(contentType, blobItem.Metadata)
|
||||
if isDir {
|
||||
// check if the dir is already included, it will be sent as blob prefix if it contains at least one item
|
||||
if _, ok := prefixes[name]; ok {
|
||||
|
@ -530,6 +556,17 @@ func (*AzureBlobFs) IsNotSupported(err error) bool {
|
|||
return err == ErrVfsUnsupported
|
||||
}
|
||||
|
||||
func (*AzureBlobFs) isBadRequestError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
var respErr *azcore.ResponseError
|
||||
if errors.As(err, &respErr) {
|
||||
return respErr.StatusCode == http.StatusBadRequest
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// CheckRootPath creates the specified local root directory if it does not exists
|
||||
func (fs *AzureBlobFs) CheckRootPath(username string, uid int, gid int) bool {
|
||||
// we need a local directory for temporary files
|
||||
|
@ -544,6 +581,9 @@ func (fs *AzureBlobFs) ScanRootDirContents() (int, int64, error) {
|
|||
size := int64(0)
|
||||
|
||||
pager := fs.containerClient.NewListBlobsFlatPager(&container.ListBlobsFlatOptions{
|
||||
Include: container.ListBlobsInclude{
|
||||
Metadata: true,
|
||||
},
|
||||
Prefix: &fs.config.KeyPrefix,
|
||||
})
|
||||
|
||||
|
@ -559,13 +599,16 @@ func (fs *AzureBlobFs) ScanRootDirContents() (int, int64, error) {
|
|||
for _, blobItem := range resp.ListBlobsFlatSegmentResponse.Segment.BlobItems {
|
||||
if blobItem.Properties != nil {
|
||||
contentType := util.GetStringFromPointer(blobItem.Properties.ContentType)
|
||||
isDir := (contentType == dirMimeType)
|
||||
isDir := checkDirectoryMarkers(contentType, blobItem.Metadata)
|
||||
blobSize := util.GetIntFromPointer(blobItem.Properties.ContentLength)
|
||||
if isDir && blobSize == 0 {
|
||||
continue
|
||||
}
|
||||
numFiles++
|
||||
size += blobSize
|
||||
if numFiles%1000 == 0 {
|
||||
fsLog(fs, logger.LevelDebug, "root dir scan in progress, files: %d, size: %d", numFiles, size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -582,8 +625,10 @@ func (fs *AzureBlobFs) getFileNamesInPrefix(fsPrefix string) (map[string]bool, e
|
|||
}
|
||||
|
||||
pager := fs.containerClient.NewListBlobsHierarchyPager("/", &container.ListBlobsHierarchyOptions{
|
||||
Include: container.ListBlobsInclude{},
|
||||
Prefix: &prefix,
|
||||
Include: container.ListBlobsInclude{
|
||||
//Metadata: true,
|
||||
},
|
||||
Prefix: &prefix,
|
||||
})
|
||||
|
||||
for pager.More() {
|
||||
|
@ -600,7 +645,7 @@ func (fs *AzureBlobFs) getFileNamesInPrefix(fsPrefix string) (map[string]bool, e
|
|||
name = strings.TrimPrefix(name, prefix)
|
||||
if blobItem.Properties != nil {
|
||||
contentType := util.GetStringFromPointer(blobItem.Properties.ContentType)
|
||||
isDir := (contentType == dirMimeType)
|
||||
isDir := checkDirectoryMarkers(contentType, blobItem.Metadata)
|
||||
if isDir {
|
||||
continue
|
||||
}
|
||||
|
@ -657,7 +702,10 @@ func (fs *AzureBlobFs) GetRelativePath(name string) string {
|
|||
func (fs *AzureBlobFs) Walk(root string, walkFn filepath.WalkFunc) error {
|
||||
prefix := fs.getPrefix(root)
|
||||
pager := fs.containerClient.NewListBlobsFlatPager(&container.ListBlobsFlatOptions{
|
||||
Prefix: &fs.config.KeyPrefix,
|
||||
Include: container.ListBlobsInclude{
|
||||
Metadata: true,
|
||||
},
|
||||
Prefix: &prefix,
|
||||
})
|
||||
|
||||
for pager.More() {
|
||||
|
@ -679,7 +727,7 @@ func (fs *AzureBlobFs) Walk(root string, walkFn filepath.WalkFunc) error {
|
|||
isDir := false
|
||||
if blobItem.Properties != nil {
|
||||
contentType := util.GetStringFromPointer(blobItem.Properties.ContentType)
|
||||
isDir = (contentType == dirMimeType)
|
||||
isDir = checkDirectoryMarkers(contentType, blobItem.Metadata)
|
||||
blobSize = util.GetIntFromPointer(blobItem.Properties.ContentLength)
|
||||
lastModified = util.GetTimeFromPointer(blobItem.Properties.LastModified)
|
||||
}
|
||||
|
@ -719,7 +767,7 @@ func (fs *AzureBlobFs) headObject(name string) (blob.GetPropertiesResponse, erro
|
|||
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
|
||||
defer cancelFn()
|
||||
|
||||
resp, err := fs.containerClient.NewBlockBlobClient(name).GetProperties(ctx, &blob.GetPropertiesOptions{})
|
||||
resp, err := fs.containerClient.NewBlockBlobClient(url.PathEscape(name)).GetProperties(ctx, &blob.GetPropertiesOptions{})
|
||||
|
||||
metric.AZHeadObjectCompleted(err)
|
||||
return resp, err
|
||||
|
@ -792,6 +840,14 @@ func (fs *AzureBlobFs) setConfigDefaults() {
|
|||
}
|
||||
}
|
||||
|
||||
func (fs *AzureBlobFs) mkdirInternal(name string) error {
|
||||
_, w, _, err := fs.Create(name, -1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return w.Close()
|
||||
}
|
||||
|
||||
func (fs *AzureBlobFs) hasContents(name string) (bool, error) {
|
||||
result := false
|
||||
prefix := fs.getPrefix(name)
|
||||
|
@ -928,7 +984,7 @@ func (fs *AzureBlobFs) handleMultipartDownload(ctx context.Context, blockBlob *b
|
|||
}
|
||||
|
||||
func (fs *AzureBlobFs) handleMultipartUpload(ctx context.Context, reader io.Reader,
|
||||
blockBlob *blockblob.Client, httpHeaders *blob.HTTPHeaders,
|
||||
blockBlob *blockblob.Client, httpHeaders *blob.HTTPHeaders, metadata map[string]string,
|
||||
) error {
|
||||
partSize := fs.config.UploadPartSize
|
||||
guard := make(chan struct{}, fs.config.UploadConcurrency)
|
||||
|
@ -1018,6 +1074,7 @@ func (fs *AzureBlobFs) handleMultipartUpload(ctx context.Context, reader io.Read
|
|||
|
||||
commitOptions := blockblob.CommitBlockListOptions{
|
||||
HTTPHeaders: httpHeaders,
|
||||
Metadata: metadata,
|
||||
}
|
||||
if fs.config.AccessTier != "" {
|
||||
commitOptions.Tier = (*blob.AccessTier)(&fs.config.AccessTier)
|
||||
|
@ -1080,6 +1137,18 @@ func (fs *AzureBlobFs) getStorageID() string {
|
|||
return fmt.Sprintf("azblob://%v", fs.config.Container)
|
||||
}
|
||||
|
||||
func checkDirectoryMarkers(contentType string, metadata map[string]*string) bool {
|
||||
if contentType == dirMimeType {
|
||||
return true
|
||||
}
|
||||
for k, v := range metadata {
|
||||
if strings.ToLower(k) == azFolderKey {
|
||||
return util.GetStringFromPointer(v) == "true"
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getAzContainerClientOptions() *container.ClientOptions {
|
||||
version := version.Get()
|
||||
return &container.ClientOptions{
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/xid"
|
||||
"github.com/sftpgo/sdk"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/util"
|
||||
|
@ -213,7 +214,7 @@ func (v *VirtualFolder) GetFilesystem(connectionID string, forbiddenSelfUsers []
|
|||
// CheckMetadataConsistency checks the consistency between the metadata stored
|
||||
// in the configured metadata plugin and the filesystem
|
||||
func (v *VirtualFolder) CheckMetadataConsistency() error {
|
||||
fs, err := v.GetFilesystem("", nil)
|
||||
fs, err := v.GetFilesystem(xid.New().String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -227,7 +228,7 @@ func (v *VirtualFolder) ScanQuota() (int, int64, error) {
|
|||
if v.hasPathPlaceholder() {
|
||||
return 0, 0, errors.New("cannot scan quota: this folder has a path placeholder")
|
||||
}
|
||||
fs, err := v.GetFilesystem("", nil)
|
||||
fs, err := v.GetFilesystem(xid.New().String(), nil)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
|
|
@ -226,38 +226,32 @@ func (fs *GCSFs) Rename(source, target string) error {
|
|||
if hasContents {
|
||||
return fmt.Errorf("cannot rename non empty directory: %#v", source)
|
||||
}
|
||||
if !strings.HasSuffix(target, "/") {
|
||||
target += "/"
|
||||
if err := fs.mkdirInternal(target); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
src := fs.svc.Bucket(fs.config.Bucket).Object(realSourceName)
|
||||
dst := fs.svc.Bucket(fs.config.Bucket).Object(target)
|
||||
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
|
||||
defer cancelFn()
|
||||
|
||||
copier := dst.CopierFrom(src)
|
||||
if fs.config.StorageClass != "" {
|
||||
copier.StorageClass = fs.config.StorageClass
|
||||
}
|
||||
if fs.config.ACL != "" {
|
||||
copier.PredefinedACL = fs.config.ACL
|
||||
}
|
||||
var contentType string
|
||||
if fi.IsDir() {
|
||||
contentType = dirMimeType
|
||||
} else {
|
||||
contentType = mime.TypeByExtension(path.Ext(source))
|
||||
}
|
||||
if contentType != "" {
|
||||
copier.ContentType = contentType
|
||||
}
|
||||
_, err = copier.Run(ctx)
|
||||
metric.GCSCopyObjectCompleted(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if plugin.Handler.HasMetadater() {
|
||||
if !fi.IsDir() {
|
||||
src := fs.svc.Bucket(fs.config.Bucket).Object(realSourceName)
|
||||
dst := fs.svc.Bucket(fs.config.Bucket).Object(target)
|
||||
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
|
||||
defer cancelFn()
|
||||
|
||||
copier := dst.CopierFrom(src)
|
||||
if fs.config.StorageClass != "" {
|
||||
copier.StorageClass = fs.config.StorageClass
|
||||
}
|
||||
if fs.config.ACL != "" {
|
||||
copier.PredefinedACL = fs.config.ACL
|
||||
}
|
||||
contentType := mime.TypeByExtension(path.Ext(source))
|
||||
if contentType != "" {
|
||||
copier.ContentType = contentType
|
||||
}
|
||||
_, err = copier.Run(ctx)
|
||||
metric.GCSCopyObjectCompleted(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if plugin.Handler.HasMetadater() {
|
||||
err = plugin.Handler.SetModificationTime(fs.getStorageID(), ensureAbsPath(target),
|
||||
util.GetTimeAsMsSinceEpoch(fi.ModTime()))
|
||||
if err != nil {
|
||||
|
@ -306,14 +300,7 @@ func (fs *GCSFs) Mkdir(name string) error {
|
|||
if !fs.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
if !strings.HasSuffix(name, "/") {
|
||||
name += "/"
|
||||
}
|
||||
_, w, _, err := fs.Create(name, -1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return w.Close()
|
||||
return fs.mkdirInternal(name)
|
||||
}
|
||||
|
||||
// Symlink creates source as a symbolic link to target.
|
||||
|
@ -534,6 +521,9 @@ func (fs *GCSFs) ScanRootDirContents() (int, int64, error) {
|
|||
}
|
||||
numFiles++
|
||||
size += attrs.Size
|
||||
if numFiles%1000 == 0 {
|
||||
fsLog(fs, logger.LevelDebug, "root dir scan in progress, files: %d, size: %d", numFiles, size)
|
||||
}
|
||||
}
|
||||
|
||||
objects = nil
|
||||
|
@ -759,6 +749,17 @@ func (fs *GCSFs) getObjectStat(name string) (string, os.FileInfo, error) {
|
|||
return name + "/", info, err
|
||||
}
|
||||
|
||||
func (fs *GCSFs) mkdirInternal(name string) error {
|
||||
if !strings.HasSuffix(name, "/") {
|
||||
name += "/"
|
||||
}
|
||||
_, w, _, err := fs.Create(name, -1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return w.Close()
|
||||
}
|
||||
|
||||
func (fs *GCSFs) hasContents(name string) (bool, error) {
|
||||
result := false
|
||||
prefix := fs.getPrefix(name)
|
||||
|
|
|
@ -382,6 +382,9 @@ func (fs *OsFs) GetDirSize(dirname string) (int, int64, error) {
|
|||
if info != nil && info.Mode().IsRegular() {
|
||||
size += info.Size()
|
||||
numFiles++
|
||||
if numFiles%1000 == 0 {
|
||||
fsLog(fs, logger.LevelDebug, "dirname %q scan in progress, files: %d, size: %d", dirname, numFiles, size)
|
||||
}
|
||||
}
|
||||
return err
|
||||
})
|
||||
|
|
|
@ -60,6 +60,10 @@ const (
|
|||
s3fsName = "S3Fs"
|
||||
)
|
||||
|
||||
var (
|
||||
s3DirMimeTypes = []string{s3DirMimeType, "httpd/unix-directory"}
|
||||
)
|
||||
|
||||
// S3Fs is a Fs implementation for AWS S3 compatible object storages
|
||||
type S3Fs struct {
|
||||
connectionID string
|
||||
|
@ -159,8 +163,10 @@ func (fs *S3Fs) Stat(name string) (os.FileInfo, error) {
|
|||
}
|
||||
obj, err := fs.headObject(name)
|
||||
if err == nil {
|
||||
// a "dir" has a trailing "/" so we cannot have a directory here
|
||||
return updateFileInfoModTime(fs.getStorageID(), name, NewFileInfo(name, false, obj.ContentLength,
|
||||
// Some S3 providers (like SeaweedFS) remove the trailing '/' from object keys.
|
||||
// So we check some common content types to detect if this is a "directory".
|
||||
isDir := util.Contains(s3DirMimeTypes, util.GetStringFromPointer(obj.ContentType))
|
||||
return updateFileInfoModTime(fs.getStorageID(), name, NewFileInfo(name, isDir, obj.ContentLength,
|
||||
util.GetTimeFromPointer(obj.LastModified), false))
|
||||
}
|
||||
if !fs.IsNotExist(err) {
|
||||
|
@ -289,63 +295,53 @@ func (fs *S3Fs) Rename(source, target string) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
copySource := fs.Join(fs.config.Bucket, source)
|
||||
if fi.IsDir() {
|
||||
hasContents, err := fs.hasContents(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hasContents {
|
||||
return fmt.Errorf("cannot rename non empty directory: %#v", source)
|
||||
return fmt.Errorf("cannot rename non empty directory: %q", source)
|
||||
}
|
||||
if !strings.HasSuffix(copySource, "/") {
|
||||
copySource += "/"
|
||||
if err := fs.mkdirInternal(target); err != nil {
|
||||
return err
|
||||
}
|
||||
if !strings.HasSuffix(target, "/") {
|
||||
target += "/"
|
||||
}
|
||||
}
|
||||
var contentType string
|
||||
if fi.IsDir() {
|
||||
contentType = s3DirMimeType
|
||||
} else {
|
||||
contentType = mime.TypeByExtension(path.Ext(source))
|
||||
}
|
||||
copySource = pathEscape(copySource)
|
||||
contentType := mime.TypeByExtension(path.Ext(source))
|
||||
copySource := pathEscape(fs.Join(fs.config.Bucket, source))
|
||||
|
||||
if fi.Size() > 500*1024*1024 {
|
||||
fsLog(fs, logger.LevelDebug, "renaming file %q with size %d using multipart copy",
|
||||
source, fi.Size())
|
||||
err = fs.doMultipartCopy(copySource, target, contentType, fi.Size())
|
||||
} else {
|
||||
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
|
||||
defer cancelFn()
|
||||
if fi.Size() > 500*1024*1024 {
|
||||
fsLog(fs, logger.LevelDebug, "renaming file %q with size %d using multipart copy",
|
||||
source, fi.Size())
|
||||
err = fs.doMultipartCopy(copySource, target, contentType, fi.Size())
|
||||
} else {
|
||||
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
|
||||
defer cancelFn()
|
||||
|
||||
_, err = fs.svc.CopyObject(ctx, &s3.CopyObjectInput{
|
||||
Bucket: aws.String(fs.config.Bucket),
|
||||
CopySource: aws.String(copySource),
|
||||
Key: aws.String(target),
|
||||
StorageClass: types.StorageClass(fs.config.StorageClass),
|
||||
ACL: types.ObjectCannedACL(fs.config.ACL),
|
||||
ContentType: util.NilIfEmpty(contentType),
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
_, err = fs.svc.CopyObject(ctx, &s3.CopyObjectInput{
|
||||
Bucket: aws.String(fs.config.Bucket),
|
||||
CopySource: aws.String(copySource),
|
||||
Key: aws.String(target),
|
||||
StorageClass: types.StorageClass(fs.config.StorageClass),
|
||||
ACL: types.ObjectCannedACL(fs.config.ACL),
|
||||
ContentType: util.NilIfEmpty(contentType),
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
metric.S3CopyObjectCompleted(err)
|
||||
return err
|
||||
}
|
||||
|
||||
waiter := s3.NewObjectExistsWaiter(fs.svc)
|
||||
err = waiter.Wait(context.Background(), &s3.HeadObjectInput{
|
||||
Bucket: aws.String(fs.config.Bucket),
|
||||
Key: aws.String(target),
|
||||
}, 10*time.Second)
|
||||
metric.S3CopyObjectCompleted(err)
|
||||
return err
|
||||
}
|
||||
|
||||
waiter := s3.NewObjectExistsWaiter(fs.svc)
|
||||
err = waiter.Wait(context.Background(), &s3.HeadObjectInput{
|
||||
Bucket: aws.String(fs.config.Bucket),
|
||||
Key: aws.String(target),
|
||||
}, 10*time.Second)
|
||||
metric.S3CopyObjectCompleted(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if plugin.Handler.HasMetadater() {
|
||||
if !fi.IsDir() {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if plugin.Handler.HasMetadater() {
|
||||
err = plugin.Handler.SetModificationTime(fs.getStorageID(), ensureAbsPath(target),
|
||||
util.GetTimeAsMsSinceEpoch(fi.ModTime()))
|
||||
if err != nil {
|
||||
|
@ -393,14 +389,7 @@ func (fs *S3Fs) Mkdir(name string) error {
|
|||
if !fs.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
if !strings.HasSuffix(name, "/") {
|
||||
name += "/"
|
||||
}
|
||||
_, w, _, err := fs.Create(name, -1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return w.Close()
|
||||
return fs.mkdirInternal(name)
|
||||
}
|
||||
|
||||
// Symlink creates source as a symbolic link to target.
|
||||
|
@ -600,6 +589,9 @@ func (fs *S3Fs) ScanRootDirContents() (int, int64, error) {
|
|||
}
|
||||
numFiles++
|
||||
size += fileObject.Size
|
||||
if numFiles%1000 == 0 {
|
||||
fsLog(fs, logger.LevelDebug, "root dir scan in progress, files: %d, size: %d", numFiles, size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -775,6 +767,17 @@ func (fs *S3Fs) setConfigDefaults() {
|
|||
}
|
||||
}
|
||||
|
||||
func (fs *S3Fs) mkdirInternal(name string) error {
|
||||
if !strings.HasSuffix(name, "/") {
|
||||
name += "/"
|
||||
}
|
||||
_, w, _, err := fs.Create(name, -1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return w.Close()
|
||||
}
|
||||
|
||||
func (fs *S3Fs) hasContents(name string) (bool, error) {
|
||||
prefix := fs.getPrefix(name)
|
||||
paginator := s3.NewListObjectsV2Paginator(fs.svc, &s3.ListObjectsV2Input{
|
||||
|
|
|
@ -16,8 +16,10 @@ package vfs
|
|||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net"
|
||||
|
@ -25,12 +27,15 @@ import (
|
|||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/eikenb/pipeat"
|
||||
"github.com/pkg/sftp"
|
||||
"github.com/robfig/cron/v3"
|
||||
"github.com/rs/xid"
|
||||
"github.com/sftpgo/sdk"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
@ -43,11 +48,16 @@ import (
|
|||
|
||||
const (
|
||||
// sftpFsName is the name for the SFTP Fs implementation
|
||||
sftpFsName = "sftpfs"
|
||||
sftpFsName = "sftpfs"
|
||||
logSenderSFTPCache = "sftpCache"
|
||||
maxSessionsPerConnection = 5
|
||||
)
|
||||
|
||||
// ErrSFTPLoop defines the error to return if an SFTP loop is detected
|
||||
var ErrSFTPLoop = errors.New("SFTP loop or nested local SFTP folders detected")
|
||||
var (
|
||||
// ErrSFTPLoop defines the error to return if an SFTP loop is detected
|
||||
ErrSFTPLoop = errors.New("SFTP loop or nested local SFTP folders detected")
|
||||
sftpConnsCache = newSFTPConnectionCache()
|
||||
)
|
||||
|
||||
// SFTPFsConfig defines the configuration for SFTP based filesystem
|
||||
type SFTPFsConfig struct {
|
||||
|
@ -145,6 +155,9 @@ func (c *SFTPFsConfig) validate() error {
|
|||
if c.Endpoint == "" {
|
||||
return errors.New("endpoint cannot be empty")
|
||||
}
|
||||
if !strings.Contains(c.Endpoint, ":") {
|
||||
c.Endpoint += ":22"
|
||||
}
|
||||
_, _, err := net.SplitHostPort(c.Endpoint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid endpoint: %v", err)
|
||||
|
@ -220,17 +233,36 @@ func (c *SFTPFsConfig) ValidateAndEncryptCredentials(additionalData string) erro
|
|||
return nil
|
||||
}
|
||||
|
||||
// getUniqueID returns an hash of the settings used to connect to the SFTP server
|
||||
func (c *SFTPFsConfig) getUniqueID(partition int) uint64 {
|
||||
h := fnv.New64a()
|
||||
var b bytes.Buffer
|
||||
|
||||
b.WriteString(c.Endpoint)
|
||||
b.WriteString(c.Username)
|
||||
b.WriteString(strings.Join(c.Fingerprints, ""))
|
||||
b.WriteString(strconv.FormatBool(c.DisableCouncurrentReads))
|
||||
b.WriteString(strconv.FormatInt(c.BufferSize, 10))
|
||||
b.WriteString(c.Password.GetPayload())
|
||||
b.WriteString(c.PrivateKey.GetPayload())
|
||||
b.WriteString(c.KeyPassphrase.GetPayload())
|
||||
if allowSelfConnections != 0 {
|
||||
b.WriteString(strings.Join(c.forbiddenSelfUsernames, ""))
|
||||
}
|
||||
b.WriteString(strconv.Itoa(partition))
|
||||
|
||||
h.Write(b.Bytes())
|
||||
return h.Sum64()
|
||||
}
|
||||
|
||||
// SFTPFs is a Fs implementation for SFTP backends
|
||||
type SFTPFs struct {
|
||||
sync.Mutex
|
||||
connectionID string
|
||||
// if not empty this fs is mouted as virtual folder in the specified path
|
||||
mountPath string
|
||||
localTempDir string
|
||||
config *SFTPFsConfig
|
||||
sshClient *ssh.Client
|
||||
sftpClient *sftp.Client
|
||||
err chan error
|
||||
conn *sftpConnection
|
||||
}
|
||||
|
||||
// NewSFTPFs returns an SFTPFs object that allows to interact with an SFTP server
|
||||
|
@ -266,15 +298,18 @@ func NewSFTPFs(connectionID, mountPath, localTempDir string, forbiddenSelfUserna
|
|||
mountPath: getMountPath(mountPath),
|
||||
localTempDir: localTempDir,
|
||||
config: &config,
|
||||
err: make(chan error, 1),
|
||||
conn: sftpConnsCache.Get(&config, connectionID),
|
||||
}
|
||||
err := sftpFs.createConnection()
|
||||
if err != nil {
|
||||
sftpFs.Close() //nolint:errcheck
|
||||
}
|
||||
return sftpFs, err
|
||||
}
|
||||
|
||||
// Name returns the name for the Fs implementation
|
||||
func (fs *SFTPFs) Name() string {
|
||||
return fmt.Sprintf("%v %#v", sftpFsName, fs.config.Endpoint)
|
||||
return fmt.Sprintf(`%s %q@%q`, sftpFsName, fs.config.Username, fs.config.Endpoint)
|
||||
}
|
||||
|
||||
// ConnectionID returns the connection ID associated to this Fs implementation
|
||||
|
@ -284,26 +319,29 @@ func (fs *SFTPFs) ConnectionID() string {
|
|||
|
||||
// Stat returns a FileInfo describing the named file
|
||||
func (fs *SFTPFs) Stat(name string) (os.FileInfo, error) {
|
||||
if err := fs.checkConnection(); err != nil {
|
||||
client, err := fs.conn.getClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fs.sftpClient.Stat(name)
|
||||
return client.Stat(name)
|
||||
}
|
||||
|
||||
// Lstat returns a FileInfo describing the named file
|
||||
func (fs *SFTPFs) Lstat(name string) (os.FileInfo, error) {
|
||||
if err := fs.checkConnection(); err != nil {
|
||||
client, err := fs.conn.getClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fs.sftpClient.Lstat(name)
|
||||
return client.Lstat(name)
|
||||
}
|
||||
|
||||
// Open opens the named file for reading
|
||||
func (fs *SFTPFs) Open(name string, offset int64) (File, *pipeat.PipeReaderAt, func(), error) {
|
||||
if err := fs.checkConnection(); err != nil {
|
||||
client, err := fs.conn.getClient()
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
f, err := fs.sftpClient.Open(name)
|
||||
f, err := client.Open(name)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
@ -337,21 +375,21 @@ func (fs *SFTPFs) Open(name string, offset int64) (File, *pipeat.PipeReaderAt, f
|
|||
|
||||
// Create creates or opens the named file for writing
|
||||
func (fs *SFTPFs) Create(name string, flag int) (File, *PipeWriter, func(), error) {
|
||||
err := fs.checkConnection()
|
||||
client, err := fs.conn.getClient()
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
if fs.config.BufferSize == 0 {
|
||||
var f File
|
||||
if flag == 0 {
|
||||
f, err = fs.sftpClient.Create(name)
|
||||
f, err = client.Create(name)
|
||||
} else {
|
||||
f, err = fs.sftpClient.OpenFile(name, flag)
|
||||
f, err = client.OpenFile(name, flag)
|
||||
}
|
||||
return f, nil, nil, err
|
||||
}
|
||||
// buffering is enabled
|
||||
f, err := fs.sftpClient.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC)
|
||||
f, err := client.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
@ -393,48 +431,53 @@ func (fs *SFTPFs) Rename(source, target string) error {
|
|||
if source == target {
|
||||
return nil
|
||||
}
|
||||
if err := fs.checkConnection(); err != nil {
|
||||
client, err := fs.conn.getClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, ok := fs.sftpClient.HasExtension("posix-rename@openssh.com"); ok {
|
||||
return fs.sftpClient.PosixRename(source, target)
|
||||
if _, ok := client.HasExtension("posix-rename@openssh.com"); ok {
|
||||
return client.PosixRename(source, target)
|
||||
}
|
||||
return fs.sftpClient.Rename(source, target)
|
||||
return client.Rename(source, target)
|
||||
}
|
||||
|
||||
// Remove removes the named file or (empty) directory.
|
||||
func (fs *SFTPFs) Remove(name string, isDir bool) error {
|
||||
if err := fs.checkConnection(); err != nil {
|
||||
client, err := fs.conn.getClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isDir {
|
||||
return fs.sftpClient.RemoveDirectory(name)
|
||||
return client.RemoveDirectory(name)
|
||||
}
|
||||
return fs.sftpClient.Remove(name)
|
||||
return client.Remove(name)
|
||||
}
|
||||
|
||||
// Mkdir creates a new directory with the specified name and default permissions
|
||||
func (fs *SFTPFs) Mkdir(name string) error {
|
||||
if err := fs.checkConnection(); err != nil {
|
||||
client, err := fs.conn.getClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fs.sftpClient.Mkdir(name)
|
||||
return client.Mkdir(name)
|
||||
}
|
||||
|
||||
// Symlink creates source as a symbolic link to target.
|
||||
func (fs *SFTPFs) Symlink(source, target string) error {
|
||||
if err := fs.checkConnection(); err != nil {
|
||||
client, err := fs.conn.getClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fs.sftpClient.Symlink(source, target)
|
||||
return client.Symlink(source, target)
|
||||
}
|
||||
|
||||
// Readlink returns the destination of the named symbolic link
|
||||
func (fs *SFTPFs) Readlink(name string) (string, error) {
|
||||
if err := fs.checkConnection(); err != nil {
|
||||
client, err := fs.conn.getClient()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
resolved, err := fs.sftpClient.ReadLink(name)
|
||||
resolved, err := client.ReadLink(name)
|
||||
if err != nil {
|
||||
return resolved, err
|
||||
}
|
||||
|
@ -448,43 +491,48 @@ func (fs *SFTPFs) Readlink(name string) (string, error) {
|
|||
|
||||
// Chown changes the numeric uid and gid of the named file.
|
||||
func (fs *SFTPFs) Chown(name string, uid int, gid int) error {
|
||||
if err := fs.checkConnection(); err != nil {
|
||||
client, err := fs.conn.getClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fs.sftpClient.Chown(name, uid, gid)
|
||||
return client.Chown(name, uid, gid)
|
||||
}
|
||||
|
||||
// Chmod changes the mode of the named file to mode.
|
||||
func (fs *SFTPFs) Chmod(name string, mode os.FileMode) error {
|
||||
if err := fs.checkConnection(); err != nil {
|
||||
client, err := fs.conn.getClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fs.sftpClient.Chmod(name, mode)
|
||||
return client.Chmod(name, mode)
|
||||
}
|
||||
|
||||
// Chtimes changes the access and modification times of the named file.
|
||||
func (fs *SFTPFs) Chtimes(name string, atime, mtime time.Time, isUploading bool) error {
|
||||
if err := fs.checkConnection(); err != nil {
|
||||
client, err := fs.conn.getClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fs.sftpClient.Chtimes(name, atime, mtime)
|
||||
return client.Chtimes(name, atime, mtime)
|
||||
}
|
||||
|
||||
// Truncate changes the size of the named file.
|
||||
func (fs *SFTPFs) Truncate(name string, size int64) error {
|
||||
if err := fs.checkConnection(); err != nil {
|
||||
client, err := fs.conn.getClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fs.sftpClient.Truncate(name, size)
|
||||
return client.Truncate(name, size)
|
||||
}
|
||||
|
||||
// ReadDir reads the directory named by dirname and returns
|
||||
// a list of directory entries.
|
||||
func (fs *SFTPFs) ReadDir(dirname string) ([]os.FileInfo, error) {
|
||||
if err := fs.checkConnection(); err != nil {
|
||||
client, err := fs.conn.getClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fs.sftpClient.ReadDir(dirname)
|
||||
return client.ReadDir(dirname)
|
||||
}
|
||||
|
||||
// IsUploadResumeSupported returns true if resuming uploads is supported.
|
||||
|
@ -528,11 +576,12 @@ func (fs *SFTPFs) CheckRootPath(username string, uid int, gid int) bool {
|
|||
if fs.config.Prefix == "/" {
|
||||
return true
|
||||
}
|
||||
if err := fs.checkConnection(); err != nil {
|
||||
client, err := fs.conn.getClient()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if err := fs.sftpClient.MkdirAll(fs.config.Prefix); err != nil {
|
||||
fsLog(fs, logger.LevelDebug, "error creating root directory %#v for user %#v: %v", fs.config.Prefix, username, err)
|
||||
if err := client.MkdirAll(fs.config.Prefix); err != nil {
|
||||
fsLog(fs, logger.LevelDebug, "error creating root directory %q for user %q: %v", fs.config.Prefix, username, err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
@ -581,10 +630,11 @@ func (fs *SFTPFs) GetRelativePath(name string) string {
|
|||
// Walk walks the file tree rooted at root, calling walkFn for each file or
|
||||
// directory in the tree, including root
|
||||
func (fs *SFTPFs) Walk(root string, walkFn filepath.WalkFunc) error {
|
||||
if err := fs.checkConnection(); err != nil {
|
||||
client, err := fs.conn.getClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
walker := fs.sftpClient.Walk(root)
|
||||
walker := client.Walk(root)
|
||||
for walker.Step() {
|
||||
err := walker.Err()
|
||||
if err != nil {
|
||||
|
@ -620,9 +670,6 @@ func (fs *SFTPFs) ResolvePath(virtualPath string) (string, error) {
|
|||
if fs.config.Prefix != "/" && fsPath != "/" {
|
||||
// we need to check if this path is a symlink outside the given prefix
|
||||
// or a file/dir inside a dir symlinked outside the prefix
|
||||
if err := fs.checkConnection(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
var validatedPath string
|
||||
var err error
|
||||
validatedPath, err = fs.getRealPath(fsPath)
|
||||
|
@ -657,10 +704,11 @@ func (fs *SFTPFs) ResolvePath(virtualPath string) (string, error) {
|
|||
|
||||
// RealPath implements the FsRealPather interface
|
||||
func (fs *SFTPFs) RealPath(p string) (string, error) {
|
||||
if err := fs.checkConnection(); err != nil {
|
||||
client, err := fs.conn.getClient()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
resolved, err := fs.sftpClient.RealPath(p)
|
||||
resolved, err := client.RealPath(p)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -676,16 +724,20 @@ func (fs *SFTPFs) RealPath(p string) (string, error) {
|
|||
|
||||
// getRealPath returns the real remote path trying to resolve symbolic links if any
|
||||
func (fs *SFTPFs) getRealPath(name string) (string, error) {
|
||||
client, err := fs.conn.getClient()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
linksWalked := 0
|
||||
for {
|
||||
info, err := fs.sftpClient.Lstat(name)
|
||||
info, err := client.Lstat(name)
|
||||
if err != nil {
|
||||
return name, err
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink == 0 {
|
||||
return name, nil
|
||||
}
|
||||
resolvedLink, err := fs.sftpClient.ReadLink(name)
|
||||
resolvedLink, err := client.ReadLink(name)
|
||||
if err != nil {
|
||||
return name, fmt.Errorf("unable to resolve link to %q: %w", name, err)
|
||||
}
|
||||
|
@ -723,12 +775,13 @@ func (fs *SFTPFs) isSubDir(name string) error {
|
|||
func (fs *SFTPFs) GetDirSize(dirname string) (int, int64, error) {
|
||||
numFiles := 0
|
||||
size := int64(0)
|
||||
if err := fs.checkConnection(); err != nil {
|
||||
client, err := fs.conn.getClient()
|
||||
if err != nil {
|
||||
return numFiles, size, err
|
||||
}
|
||||
isDir, err := isDirectory(fs, dirname)
|
||||
if err == nil && isDir {
|
||||
walker := fs.sftpClient.Walk(dirname)
|
||||
walker := client.Walk(dirname)
|
||||
for walker.Step() {
|
||||
err := walker.Err()
|
||||
if err != nil {
|
||||
|
@ -737,6 +790,9 @@ func (fs *SFTPFs) GetDirSize(dirname string) (int, int64, error) {
|
|||
if walker.Stat().Mode().IsRegular() {
|
||||
size += walker.Stat().Size()
|
||||
numFiles++
|
||||
if numFiles%1000 == 0 {
|
||||
fsLog(fs, logger.LevelDebug, "dirname %q scan in progress, files: %d, size: %d", dirname, numFiles, size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -745,10 +801,11 @@ func (fs *SFTPFs) GetDirSize(dirname string) (int, int64, error) {
|
|||
|
||||
// GetMimeType returns the content type
|
||||
func (fs *SFTPFs) GetMimeType(name string) (string, error) {
|
||||
if err := fs.checkConnection(); err != nil {
|
||||
client, err := fs.conn.getClient()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
f, err := fs.sftpClient.OpenFile(name, os.O_RDONLY)
|
||||
f, err := client.OpenFile(name, os.O_RDONLY)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -766,31 +823,20 @@ func (fs *SFTPFs) GetMimeType(name string) (string, error) {
|
|||
|
||||
// GetAvailableDiskSize returns the available size for the specified path
|
||||
func (fs *SFTPFs) GetAvailableDiskSize(dirName string) (*sftp.StatVFS, error) {
|
||||
if err := fs.checkConnection(); err != nil {
|
||||
client, err := fs.conn.getClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, ok := fs.sftpClient.HasExtension("statvfs@openssh.com"); !ok {
|
||||
if _, ok := client.HasExtension("statvfs@openssh.com"); !ok {
|
||||
return nil, ErrStorageSizeUnavailable
|
||||
}
|
||||
return fs.sftpClient.StatVFS(dirName)
|
||||
return client.StatVFS(dirName)
|
||||
}
|
||||
|
||||
// Close the connection
|
||||
func (fs *SFTPFs) Close() error {
|
||||
fs.Lock()
|
||||
defer fs.Unlock()
|
||||
|
||||
var sftpErr, sshErr error
|
||||
if fs.sftpClient != nil {
|
||||
sftpErr = fs.sftpClient.Close()
|
||||
}
|
||||
if fs.sshClient != nil {
|
||||
sshErr = fs.sshClient.Close()
|
||||
}
|
||||
if sftpErr != nil {
|
||||
return sftpErr
|
||||
}
|
||||
return sshErr
|
||||
fs.conn.RemoveSession(fs.connectionID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *SFTPFs) copy(dst io.Writer, src io.Reader) (written int64, err error) {
|
||||
|
@ -825,64 +871,98 @@ func (fs *SFTPFs) copy(dst io.Writer, src io.Reader) (written int64, err error)
|
|||
return written, err
|
||||
}
|
||||
|
||||
func (fs *SFTPFs) checkConnection() error {
|
||||
err := fs.closed()
|
||||
if err == nil {
|
||||
return nil
|
||||
func (fs *SFTPFs) createConnection() error {
|
||||
err := fs.conn.OpenConnection()
|
||||
if err != nil {
|
||||
fsLog(fs, logger.LevelError, "error opening connection: %v", err)
|
||||
return err
|
||||
}
|
||||
return fs.createConnection()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *SFTPFs) createConnection() error {
|
||||
fs.Lock()
|
||||
defer fs.Unlock()
|
||||
type sftpConnection struct {
|
||||
config *SFTPFsConfig
|
||||
logSender string
|
||||
sshClient *ssh.Client
|
||||
sftpClient *sftp.Client
|
||||
mu sync.RWMutex
|
||||
isConnected bool
|
||||
sessions map[string]bool
|
||||
lastActivity time.Time
|
||||
}
|
||||
|
||||
var err error
|
||||
func newSFTPConnection(config *SFTPFsConfig, sessionID string) *sftpConnection {
|
||||
c := &sftpConnection{
|
||||
config: config,
|
||||
logSender: fmt.Sprintf(`%s "%s@%s"`, sftpFsName, config.Username, config.Endpoint),
|
||||
isConnected: false,
|
||||
sessions: map[string]bool{},
|
||||
lastActivity: time.Now().UTC(),
|
||||
}
|
||||
c.sessions[sessionID] = true
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *sftpConnection) OpenConnection() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
return c.openConnNoLock()
|
||||
}
|
||||
|
||||
func (c *sftpConnection) openConnNoLock() error {
|
||||
if c.isConnected {
|
||||
logger.Debug(c.logSender, "", "reusing connection")
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Debug(c.logSender, "", "try to open a new connection")
|
||||
clientConfig := &ssh.ClientConfig{
|
||||
User: fs.config.Username,
|
||||
User: c.config.Username,
|
||||
HostKeyCallback: func(_ string, _ net.Addr, key ssh.PublicKey) error {
|
||||
fp := ssh.FingerprintSHA256(key)
|
||||
if util.Contains(sftpFingerprints, fp) {
|
||||
if allowSelfConnections == 0 {
|
||||
fsLog(fs, logger.LevelError, "SFTP self connections not allowed")
|
||||
logger.Log(logger.LevelError, c.logSender, "", "SFTP self connections not allowed")
|
||||
return ErrSFTPLoop
|
||||
}
|
||||
if util.Contains(fs.config.forbiddenSelfUsernames, fs.config.Username) {
|
||||
fsLog(fs, logger.LevelError, "SFTP loop or nested local SFTP folders detected, mount path %q, username %q, forbidden usernames: %+v",
|
||||
fs.mountPath, fs.config.Username, fs.config.forbiddenSelfUsernames)
|
||||
if util.Contains(c.config.forbiddenSelfUsernames, c.config.Username) {
|
||||
logger.Log(logger.LevelError, c.logSender, "",
|
||||
"SFTP loop or nested local SFTP folders detected, username %q, forbidden usernames: %+v",
|
||||
c.config.Username, c.config.forbiddenSelfUsernames)
|
||||
return ErrSFTPLoop
|
||||
}
|
||||
}
|
||||
if len(fs.config.Fingerprints) > 0 {
|
||||
for _, provided := range fs.config.Fingerprints {
|
||||
if len(c.config.Fingerprints) > 0 {
|
||||
for _, provided := range c.config.Fingerprints {
|
||||
if provided == fp {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("invalid fingerprint %#v", fp)
|
||||
return fmt.Errorf("invalid fingerprint %q", fp)
|
||||
}
|
||||
fsLog(fs, logger.LevelWarn, "login without host key validation, please provide at least a fingerprint!")
|
||||
logger.Log(logger.LevelWarn, c.logSender, "", "login without host key validation, please provide at least a fingerprint!")
|
||||
return nil
|
||||
},
|
||||
Timeout: 10 * time.Second,
|
||||
ClientVersion: fmt.Sprintf("SSH-2.0-SFTPGo_%v", version.Get().Version),
|
||||
}
|
||||
if fs.config.PrivateKey.GetPayload() != "" {
|
||||
if c.config.PrivateKey.GetPayload() != "" {
|
||||
var signer ssh.Signer
|
||||
if fs.config.KeyPassphrase.GetPayload() != "" {
|
||||
signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(fs.config.PrivateKey.GetPayload()),
|
||||
[]byte(fs.config.KeyPassphrase.GetPayload()))
|
||||
var err error
|
||||
if c.config.KeyPassphrase.GetPayload() != "" {
|
||||
signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(c.config.PrivateKey.GetPayload()),
|
||||
[]byte(c.config.KeyPassphrase.GetPayload()))
|
||||
} else {
|
||||
signer, err = ssh.ParsePrivateKey([]byte(fs.config.PrivateKey.GetPayload()))
|
||||
signer, err = ssh.ParsePrivateKey([]byte(c.config.PrivateKey.GetPayload()))
|
||||
}
|
||||
if err != nil {
|
||||
fs.err <- err
|
||||
return fmt.Errorf("sftpfs: unable to parse the private key: %w", err)
|
||||
}
|
||||
clientConfig.Auth = append(clientConfig.Auth, ssh.PublicKeys(signer))
|
||||
}
|
||||
if fs.config.Password.GetPayload() != "" {
|
||||
clientConfig.Auth = append(clientConfig.Auth, ssh.Password(fs.config.Password.GetPayload()))
|
||||
if c.config.Password.GetPayload() != "" {
|
||||
clientConfig.Auth = append(clientConfig.Auth, ssh.Password(c.config.Password.GetPayload()))
|
||||
}
|
||||
// add more ciphers, KEXs and MACs, they are negotiated according to the order
|
||||
clientConfig.Ciphers = []string{"aes128-gcm@openssh.com", "aes256-gcm@openssh.com", "chacha20-poly1305@openssh.com",
|
||||
|
@ -895,52 +975,225 @@ func (fs *SFTPFs) createConnection() error {
|
|||
clientConfig.MACs = []string{"hmac-sha2-256-etm@openssh.com", "hmac-sha2-256",
|
||||
"hmac-sha2-512-etm@openssh.com", "hmac-sha2-512",
|
||||
"hmac-sha1", "hmac-sha1-96"}
|
||||
fs.sshClient, err = ssh.Dial("tcp", fs.config.Endpoint, clientConfig)
|
||||
sshClient, err := ssh.Dial("tcp", c.config.Endpoint, clientConfig)
|
||||
if err != nil {
|
||||
fsLog(fs, logger.LevelError, "unable to connect: %v", err)
|
||||
fs.err <- err
|
||||
return err
|
||||
return fmt.Errorf("sftpfs: unable to connect: %w", err)
|
||||
}
|
||||
fs.sftpClient, err = sftp.NewClient(fs.sshClient)
|
||||
sftpClient, err := sftp.NewClient(sshClient, c.getClientOptions()...)
|
||||
if err != nil {
|
||||
fsLog(fs, logger.LevelError, "unable to create SFTP client: %v", err)
|
||||
fs.sshClient.Close()
|
||||
fs.err <- err
|
||||
return err
|
||||
sshClient.Close()
|
||||
return fmt.Errorf("sftpfs: unable to create SFTP client: %w", err)
|
||||
}
|
||||
if fs.config.DisableCouncurrentReads {
|
||||
fsLog(fs, logger.LevelDebug, "disabling concurrent reads")
|
||||
opt := sftp.UseConcurrentReads(false)
|
||||
opt(fs.sftpClient) //nolint:errcheck
|
||||
}
|
||||
if fs.config.BufferSize > 0 {
|
||||
fsLog(fs, logger.LevelDebug, "enabling concurrent writes")
|
||||
opt := sftp.UseConcurrentWrites(true)
|
||||
opt(fs.sftpClient) //nolint:errcheck
|
||||
}
|
||||
go fs.wait()
|
||||
c.sshClient = sshClient
|
||||
c.sftpClient = sftpClient
|
||||
c.isConnected = true
|
||||
go c.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *SFTPFs) wait() {
|
||||
func (c *sftpConnection) getClientOptions() []sftp.ClientOption {
|
||||
var options []sftp.ClientOption
|
||||
if c.config.DisableCouncurrentReads {
|
||||
options = append(options, sftp.UseConcurrentReads(false))
|
||||
logger.Debug(c.logSender, "", "disabling concurrent reads")
|
||||
}
|
||||
if c.config.BufferSize > 0 {
|
||||
options = append(options, sftp.UseConcurrentWrites(true))
|
||||
logger.Debug(c.logSender, "", "enabling concurrent writes")
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func (c *sftpConnection) getClient() (*sftp.Client, error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.isConnected {
|
||||
return c.sftpClient, nil
|
||||
}
|
||||
err := c.openConnNoLock()
|
||||
return c.sftpClient, err
|
||||
}
|
||||
|
||||
func (c *sftpConnection) Wait() {
|
||||
done := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
var watchdogInProgress atomic.Bool
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if watchdogInProgress.Load() {
|
||||
logger.Error(c.logSender, "", "watchdog still in progress, closing hanging connection")
|
||||
c.sshClient.Close()
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
watchdogInProgress.Store(true)
|
||||
defer watchdogInProgress.Store(false)
|
||||
|
||||
_, err := c.sftpClient.Getwd()
|
||||
if err != nil {
|
||||
logger.Error(c.logSender, "", "watchdog error: %v", err)
|
||||
}
|
||||
}()
|
||||
case <-done:
|
||||
logger.Debug(c.logSender, "", "quitting watchdog")
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// we wait on the sftp client otherwise if the channel is closed but not the connection
|
||||
// we don't detect the event.
|
||||
fs.err <- fs.sftpClient.Wait()
|
||||
fsLog(fs, logger.LevelDebug, "sftp channel closed")
|
||||
err := c.sftpClient.Wait()
|
||||
logger.Log(logger.LevelDebug, c.logSender, "", "sftp channel closed: %v", err)
|
||||
close(done)
|
||||
|
||||
fs.Lock()
|
||||
defer fs.Unlock()
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if fs.sshClient != nil {
|
||||
fs.sshClient.Close()
|
||||
c.isConnected = false
|
||||
if c.sshClient != nil {
|
||||
c.sshClient.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (fs *SFTPFs) closed() error {
|
||||
select {
|
||||
case err := <-fs.err:
|
||||
return err
|
||||
default:
|
||||
return nil
|
||||
func (c *sftpConnection) Close() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
logger.Debug(c.logSender, "", "closing connection")
|
||||
var sftpErr, sshErr error
|
||||
if c.sftpClient != nil {
|
||||
sftpErr = c.sftpClient.Close()
|
||||
}
|
||||
if c.sshClient != nil {
|
||||
sshErr = c.sshClient.Close()
|
||||
}
|
||||
if sftpErr != nil {
|
||||
return sftpErr
|
||||
}
|
||||
c.isConnected = false
|
||||
return sshErr
|
||||
}
|
||||
|
||||
func (c *sftpConnection) AddSession(sessionID string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.sessions[sessionID] = true
|
||||
logger.Debug(c.logSender, "", "added session %s, active sessions: %d", sessionID, len(c.sessions))
|
||||
}
|
||||
|
||||
func (c *sftpConnection) RemoveSession(sessionID string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
delete(c.sessions, sessionID)
|
||||
logger.Debug(c.logSender, "", "removed session %s, active sessions: %d", sessionID, len(c.sessions))
|
||||
if len(c.sessions) == 0 {
|
||||
c.lastActivity = time.Now().UTC()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *sftpConnection) ActiveSessions() int {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
return len(c.sessions)
|
||||
}
|
||||
|
||||
func (c *sftpConnection) GetLastActivity() time.Time {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
if len(c.sessions) > 0 {
|
||||
return time.Now().UTC()
|
||||
}
|
||||
logger.Debug(c.logSender, "", "last activity %s", c.lastActivity)
|
||||
return c.lastActivity
|
||||
}
|
||||
|
||||
type sftpConnectionsCache struct {
|
||||
scheduler *cron.Cron
|
||||
sync.RWMutex
|
||||
items map[uint64]*sftpConnection
|
||||
}
|
||||
|
||||
func newSFTPConnectionCache() *sftpConnectionsCache {
|
||||
c := &sftpConnectionsCache{
|
||||
scheduler: cron.New(),
|
||||
items: make(map[uint64]*sftpConnection),
|
||||
}
|
||||
_, err := c.scheduler.AddFunc("@every 1m", c.Cleanup)
|
||||
util.PanicOnError(err)
|
||||
c.scheduler.Start()
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *sftpConnectionsCache) Get(config *SFTPFsConfig, sessionID string) *sftpConnection {
|
||||
partition := 0
|
||||
key := config.getUniqueID(partition)
|
||||
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
var oldKey uint64
|
||||
for {
|
||||
if val, ok := c.items[key]; ok {
|
||||
activeSessions := val.ActiveSessions()
|
||||
if activeSessions < maxSessionsPerConnection || key == oldKey {
|
||||
logger.Debug(logSenderSFTPCache, "",
|
||||
"reusing connection for session ID %q, key: %d, active sessions %d, active connections: %d",
|
||||
sessionID, key, activeSessions+1, len(c.items))
|
||||
val.AddSession(sessionID)
|
||||
return val
|
||||
}
|
||||
partition++
|
||||
oldKey = key
|
||||
key = config.getUniqueID(partition)
|
||||
logger.Debug(logSenderSFTPCache, "",
|
||||
"connection full, generated new key for partition: %d, active sessions: %d, key: %d, old key: %d",
|
||||
partition, activeSessions, oldKey, key)
|
||||
} else {
|
||||
conn := newSFTPConnection(config, sessionID)
|
||||
c.items[key] = conn
|
||||
logger.Debug(logSenderSFTPCache, "",
|
||||
"adding new connection for session ID %q, partition: %d, key: %d, active connections: %d",
|
||||
sessionID, partition, key, len(c.items))
|
||||
return conn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *sftpConnectionsCache) Remove(key uint64) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
if conn, ok := c.items[key]; ok {
|
||||
delete(c.items, key)
|
||||
logger.Debug(logSenderSFTPCache, "", "removed connection with key %d, active connections: %d", key, len(c.items))
|
||||
|
||||
defer conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *sftpConnectionsCache) Cleanup() {
|
||||
c.RLock()
|
||||
|
||||
for k, conn := range c.items {
|
||||
if val := conn.GetLastActivity(); val.Before(time.Now().Add(-30 * time.Second)) {
|
||||
logger.Debug(conn.logSender, "", "removing inactive connection, last activity %s", val)
|
||||
|
||||
defer func(key uint64) {
|
||||
c.Remove(key)
|
||||
}(k)
|
||||
}
|
||||
}
|
||||
|
||||
c.RUnlock()
|
||||
}
|
||||
|
|
|
@ -26,8 +26,8 @@ import (
|
|||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/drakkan/webdav"
|
||||
"github.com/eikenb/pipeat"
|
||||
"golang.org/x/net/webdav"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/common"
|
||||
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
||||
|
@ -84,6 +84,9 @@ type webDavFileInfo struct {
|
|||
// ContentType implements webdav.ContentTyper interface
|
||||
func (fi *webDavFileInfo) ContentType(ctx context.Context) (string, error) {
|
||||
extension := path.Ext(fi.virtualPath)
|
||||
if extension == "" || extension == ".dat" {
|
||||
return "application/octet-stream", nil
|
||||
}
|
||||
contentType := mime.TypeByExtension(extension)
|
||||
if contentType != "" {
|
||||
return contentType, nil
|
||||
|
@ -105,20 +108,19 @@ func (f *webDavFile) Readdir(count int) ([]os.FileInfo, error) {
|
|||
if !f.Connection.User.HasPerm(dataprovider.PermListItems, f.GetVirtualPath()) {
|
||||
return nil, f.Connection.GetPermissionDeniedError()
|
||||
}
|
||||
fileInfos, err := f.Connection.ListDir(f.GetVirtualPath())
|
||||
entries, err := f.Connection.ListDir(f.GetVirtualPath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]os.FileInfo, 0, len(fileInfos))
|
||||
for _, fileInfo := range fileInfos {
|
||||
result = append(result, &webDavFileInfo{
|
||||
FileInfo: fileInfo,
|
||||
for idx, info := range entries {
|
||||
entries[idx] = &webDavFileInfo{
|
||||
FileInfo: info,
|
||||
Fs: f.Fs,
|
||||
virtualPath: path.Join(f.GetVirtualPath(), fileInfo.Name()),
|
||||
fsPath: f.Fs.Join(f.GetFsPath(), fileInfo.Name()),
|
||||
})
|
||||
virtualPath: path.Join(f.GetVirtualPath(), info.Name()),
|
||||
fsPath: f.Fs.Join(f.GetFsPath(), info.Name()),
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// Stat the handle
|
||||
|
@ -131,7 +133,7 @@ func (f *webDavFile) Stat() (os.FileInfo, error) {
|
|||
f.Unlock()
|
||||
if f.GetType() == common.TransferUpload && errUpload == nil {
|
||||
info := &webDavFileInfo{
|
||||
FileInfo: vfs.NewFileInfo(f.GetFsPath(), false, f.BytesReceived.Load(), time.Unix(0, 0), false),
|
||||
FileInfo: vfs.NewFileInfo(f.GetFsPath(), false, f.BytesReceived.Load(), time.Now(), false),
|
||||
Fs: f.Fs,
|
||||
virtualPath: f.GetVirtualPath(),
|
||||
fsPath: f.GetFsPath(),
|
||||
|
@ -154,33 +156,38 @@ func (f *webDavFile) Stat() (os.FileInfo, error) {
|
|||
return fi, nil
|
||||
}
|
||||
|
||||
func (f *webDavFile) checkFirstRead() error {
|
||||
if !f.Connection.User.HasPerm(dataprovider.PermDownload, path.Dir(f.GetVirtualPath())) {
|
||||
return f.Connection.GetPermissionDeniedError()
|
||||
}
|
||||
transferQuota := f.BaseTransfer.GetTransferQuota()
|
||||
if !transferQuota.HasDownloadSpace() {
|
||||
f.Connection.Log(logger.LevelInfo, "denying file read due to quota limits")
|
||||
return f.Connection.GetReadQuotaExceededError()
|
||||
}
|
||||
if ok, policy := f.Connection.User.IsFileAllowed(f.GetVirtualPath()); !ok {
|
||||
f.Connection.Log(logger.LevelWarn, "reading file %#v is not allowed", f.GetVirtualPath())
|
||||
return f.Connection.GetErrorForDeniedFile(policy)
|
||||
}
|
||||
err := common.ExecutePreAction(f.Connection, common.OperationPreDownload, f.GetFsPath(), f.GetVirtualPath(), 0, 0)
|
||||
if err != nil {
|
||||
f.Connection.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", f.GetVirtualPath(), err)
|
||||
return f.Connection.GetPermissionDeniedError()
|
||||
}
|
||||
f.readTryed.Store(true)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read reads the contents to downloads.
|
||||
func (f *webDavFile) Read(p []byte) (n int, err error) {
|
||||
if f.AbortTransfer.Load() {
|
||||
return 0, errTransferAborted
|
||||
}
|
||||
if !f.readTryed.Load() {
|
||||
if !f.Connection.User.HasPerm(dataprovider.PermDownload, path.Dir(f.GetVirtualPath())) {
|
||||
return 0, f.Connection.GetPermissionDeniedError()
|
||||
if err := f.checkFirstRead(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
transferQuota := f.BaseTransfer.GetTransferQuota()
|
||||
if !transferQuota.HasDownloadSpace() {
|
||||
f.Connection.Log(logger.LevelInfo, "denying file read due to quota limits")
|
||||
return 0, f.Connection.GetReadQuotaExceededError()
|
||||
}
|
||||
|
||||
if ok, policy := f.Connection.User.IsFileAllowed(f.GetVirtualPath()); !ok {
|
||||
f.Connection.Log(logger.LevelWarn, "reading file %#v is not allowed", f.GetVirtualPath())
|
||||
return 0, f.Connection.GetErrorForDeniedFile(policy)
|
||||
}
|
||||
err := common.ExecutePreAction(f.Connection, common.OperationPreDownload, f.GetFsPath(), f.GetVirtualPath(), 0, 0)
|
||||
if err != nil {
|
||||
f.Connection.Log(logger.LevelDebug, "download for file %#v denied by pre action: %v", f.GetVirtualPath(), err)
|
||||
return 0, f.Connection.GetPermissionDeniedError()
|
||||
}
|
||||
f.readTryed.Store(true)
|
||||
}
|
||||
|
||||
f.Connection.UpdateLastActivity()
|
||||
|
||||
// the file is read sequentially we don't need to check for concurrent reads and so
|
||||
|
@ -190,10 +197,16 @@ func (f *webDavFile) Read(p []byte) (n int, err error) {
|
|||
f.TransferError(common.ErrOpUnsupported)
|
||||
return 0, common.ErrOpUnsupported
|
||||
}
|
||||
_, r, cancelFn, e := f.Fs.Open(f.GetFsPath(), 0)
|
||||
file, r, cancelFn, e := f.Fs.Open(f.GetFsPath(), 0)
|
||||
f.Lock()
|
||||
if e == nil {
|
||||
f.reader = r
|
||||
if file != nil {
|
||||
f.File = file
|
||||
f.writer = f.File
|
||||
f.reader = f.File
|
||||
} else if r != nil {
|
||||
f.reader = r
|
||||
}
|
||||
f.BaseTransfer.SetCancelFn(cancelFn)
|
||||
}
|
||||
f.ErrTransfer = e
|
||||
|
@ -263,18 +276,41 @@ func (f *webDavFile) updateTransferQuotaOnSeek() {
|
|||
}
|
||||
}
|
||||
|
||||
func (f *webDavFile) checkFile() error {
|
||||
if f.File == nil && vfs.IsLocalOrUnbufferedSFTPFs(f.Fs) {
|
||||
file, _, _, err := f.Fs.Open(f.GetFsPath(), 0)
|
||||
if err != nil {
|
||||
f.Connection.Log(logger.LevelWarn, "could not open file %q for seeking: %v",
|
||||
f.GetFsPath(), err)
|
||||
f.TransferError(err)
|
||||
return err
|
||||
}
|
||||
f.File = file
|
||||
f.reader = file
|
||||
f.writer = file
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *webDavFile) seekFile(offset int64, whence int) (int64, error) {
|
||||
ret, err := f.File.Seek(offset, whence)
|
||||
if err != nil {
|
||||
f.TransferError(err)
|
||||
}
|
||||
return ret, err
|
||||
}
|
||||
|
||||
// Seek sets the offset for the next Read or Write on the writer to offset,
|
||||
// interpreted according to whence: 0 means relative to the origin of the file,
|
||||
// 1 means relative to the current offset, and 2 means relative to the end.
|
||||
// It returns the new offset and an error, if any.
|
||||
func (f *webDavFile) Seek(offset int64, whence int) (int64, error) {
|
||||
f.Connection.UpdateLastActivity()
|
||||
if err := f.checkFile(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if f.File != nil {
|
||||
ret, err := f.File.Seek(offset, whence)
|
||||
if err != nil {
|
||||
f.TransferError(err)
|
||||
}
|
||||
return ret, err
|
||||
return f.seekFile(offset, whence)
|
||||
}
|
||||
if f.GetType() == common.TransferDownload {
|
||||
readOffset := f.startOffset + f.BytesSent.Load()
|
||||
|
|
|
@ -21,8 +21,7 @@ import (
|
|||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/eikenb/pipeat"
|
||||
"golang.org/x/net/webdav"
|
||||
"github.com/drakkan/webdav"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/common"
|
||||
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
||||
|
@ -134,25 +133,13 @@ func (c *Connection) OpenFile(ctx context.Context, name string, flag int, perm o
|
|||
}
|
||||
|
||||
func (c *Connection) getFile(fs vfs.Fs, fsPath, virtualPath string) (webdav.File, error) {
|
||||
var err error
|
||||
var file vfs.File
|
||||
var r *pipeat.PipeReaderAt
|
||||
var cancelFn func()
|
||||
|
||||
// for cloud fs we open the file when we receive the first read to avoid to download the first part of
|
||||
// the file if it was opened only to do a stat or a readdir and so it is not a real download
|
||||
if vfs.IsLocalOrUnbufferedSFTPFs(fs) {
|
||||
file, r, cancelFn, err = fs.Open(fsPath, 0)
|
||||
if err != nil {
|
||||
c.Log(logger.LevelError, "could not open file %#v for reading: %+v", fsPath, err)
|
||||
return nil, c.GetFsError(fs, err)
|
||||
}
|
||||
}
|
||||
|
||||
baseTransfer := common.NewBaseTransfer(file, c.BaseConnection, cancelFn, fsPath, fsPath, virtualPath,
|
||||
// we open the file when we receive the first read so we only open the file if necessary
|
||||
baseTransfer := common.NewBaseTransfer(nil, c.BaseConnection, cancelFn, fsPath, fsPath, virtualPath,
|
||||
common.TransferDownload, 0, 0, 0, 0, false, fs, c.GetTransferQuota())
|
||||
|
||||
return newWebDavFile(baseTransfer, nil, r), nil
|
||||
return newWebDavFile(baseTransfer, nil, nil), nil
|
||||
}
|
||||
|
||||
func (c *Connection) putFile(fs vfs.Fs, fsPath, virtualPath string) (webdav.File, error) {
|
||||
|
|
|
@ -30,10 +30,10 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/drakkan/webdav"
|
||||
"github.com/eikenb/pipeat"
|
||||
"github.com/sftpgo/sdk"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/net/webdav"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/common"
|
||||
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
||||
|
@ -286,7 +286,10 @@ func (fs *MockOsFs) Name() string {
|
|||
|
||||
// Open returns nil
|
||||
func (fs *MockOsFs) Open(name string, offset int64) (vfs.File, *pipeat.PipeReaderAt, func(), error) {
|
||||
return nil, fs.reader, nil, nil
|
||||
if fs.reader != nil {
|
||||
return nil, fs.reader, nil, nil
|
||||
}
|
||||
return fs.Fs.Open(name, offset)
|
||||
}
|
||||
|
||||
// IsUploadResumeSupported returns true if resuming uploads is supported
|
||||
|
@ -314,14 +317,18 @@ func (fs *MockOsFs) Rename(source, target string) error {
|
|||
|
||||
// GetMimeType returns the content type
|
||||
func (fs *MockOsFs) GetMimeType(name string) (string, error) {
|
||||
if fs.err != nil {
|
||||
return "", fs.err
|
||||
}
|
||||
return "application/custom-mime", nil
|
||||
}
|
||||
|
||||
func newMockOsFs(atomicUpload bool, connectionID, rootDir string, reader *pipeat.PipeReaderAt) vfs.Fs {
|
||||
func newMockOsFs(atomicUpload bool, connectionID, rootDir string, reader *pipeat.PipeReaderAt, err error) vfs.Fs {
|
||||
return &MockOsFs{
|
||||
Fs: vfs.NewOsFs(connectionID, rootDir, ""),
|
||||
isAtomicUploadSupported: atomicUpload,
|
||||
reader: reader,
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -552,38 +559,31 @@ func TestFileAccessErrors(t *testing.T) {
|
|||
missingPath := "missing path"
|
||||
fsMissingPath := filepath.Join(user.HomeDir, missingPath)
|
||||
err := connection.RemoveAll(ctx, missingPath)
|
||||
if assert.Error(t, err) {
|
||||
assert.EqualError(t, err, os.ErrNotExist.Error())
|
||||
}
|
||||
_, err = connection.getFile(fs, fsMissingPath, missingPath)
|
||||
if assert.Error(t, err) {
|
||||
assert.EqualError(t, err, os.ErrNotExist.Error())
|
||||
}
|
||||
_, err = connection.getFile(fs, fsMissingPath, missingPath)
|
||||
if assert.Error(t, err) {
|
||||
assert.EqualError(t, err, os.ErrNotExist.Error())
|
||||
}
|
||||
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||
davFile, err := connection.getFile(fs, fsMissingPath, missingPath)
|
||||
assert.NoError(t, err)
|
||||
buf := make([]byte, 64)
|
||||
_, err = davFile.Read(buf)
|
||||
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||
err = davFile.Close()
|
||||
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||
p := filepath.Join(user.HomeDir, "adir", missingPath)
|
||||
_, err = connection.handleUploadToNewFile(fs, p, p, path.Join("adir", missingPath))
|
||||
if assert.Error(t, err) {
|
||||
assert.EqualError(t, err, os.ErrNotExist.Error())
|
||||
}
|
||||
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||
_, err = connection.handleUploadToExistingFile(fs, p, "_"+p, 0, path.Join("adir", missingPath))
|
||||
if assert.Error(t, err) {
|
||||
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||
}
|
||||
|
||||
fs = newMockOsFs(false, fs.ConnectionID(), user.HomeDir, nil)
|
||||
fs = newMockOsFs(false, fs.ConnectionID(), user.HomeDir, nil, nil)
|
||||
_, err = connection.handleUploadToExistingFile(fs, p, p, 0, path.Join("adir", missingPath))
|
||||
if assert.Error(t, err) {
|
||||
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||
}
|
||||
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||
|
||||
f, err := os.CreateTemp("", "temp")
|
||||
assert.NoError(t, err)
|
||||
err = f.Close()
|
||||
assert.NoError(t, err)
|
||||
davFile, err := connection.handleUploadToExistingFile(fs, f.Name(), f.Name(), 123, f.Name())
|
||||
davFile, err = connection.handleUploadToExistingFile(fs, f.Name(), f.Name(), 123, f.Name())
|
||||
if assert.NoError(t, err) {
|
||||
transfer := davFile.(*webDavFile)
|
||||
transfers := connection.GetTransfers()
|
||||
|
@ -650,9 +650,9 @@ func TestContentType(t *testing.T) {
|
|||
}
|
||||
testFilePath := filepath.Join(user.HomeDir, testFile)
|
||||
ctx := context.Background()
|
||||
baseTransfer := common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile,
|
||||
baseTransfer := common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile+".unknown",
|
||||
common.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{})
|
||||
fs = newMockOsFs(false, fs.ConnectionID(), user.GetHomeDir(), nil)
|
||||
fs = newMockOsFs(false, fs.ConnectionID(), user.GetHomeDir(), nil, nil)
|
||||
err := os.WriteFile(testFilePath, []byte(""), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
davFile := newWebDavFile(baseTransfer, nil, nil)
|
||||
|
@ -668,6 +668,8 @@ func TestContentType(t *testing.T) {
|
|||
err = davFile.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
baseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile+".unknown1",
|
||||
common.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{})
|
||||
davFile = newWebDavFile(baseTransfer, nil, nil)
|
||||
davFile.Fs = vfs.NewOsFs("id", user.HomeDir, "")
|
||||
fi, err = davFile.Stat()
|
||||
|
@ -679,9 +681,53 @@ func TestContentType(t *testing.T) {
|
|||
err = davFile.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
fi.(*webDavFileInfo).fsPath = "missing"
|
||||
_, err = fi.(*webDavFileInfo).ContentType(ctx)
|
||||
assert.EqualError(t, err, webdav.ErrNotImplemented.Error())
|
||||
baseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile,
|
||||
common.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{})
|
||||
davFile = newWebDavFile(baseTransfer, nil, nil)
|
||||
davFile.Fs = vfs.NewOsFs("id", user.HomeDir, "")
|
||||
fi, err = davFile.Stat()
|
||||
if assert.NoError(t, err) {
|
||||
ctype, err := fi.(*webDavFileInfo).ContentType(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "application/octet-stream", ctype)
|
||||
}
|
||||
err = davFile.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
// the second time the cache will be used
|
||||
baseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile+".custom",
|
||||
common.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{})
|
||||
davFile = newWebDavFile(baseTransfer, nil, nil)
|
||||
davFile.Fs = vfs.NewOsFs("id", user.HomeDir, "")
|
||||
fi, err = davFile.Stat()
|
||||
if assert.NoError(t, err) {
|
||||
ctype, err := fi.(*webDavFileInfo).ContentType(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "text/plain; charset=utf-8", ctype)
|
||||
}
|
||||
err = davFile.Close()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
baseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile+".unknown2",
|
||||
common.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{})
|
||||
fs = newMockOsFs(false, fs.ConnectionID(), user.GetHomeDir(), nil, os.ErrInvalid)
|
||||
davFile = newWebDavFile(baseTransfer, nil, nil)
|
||||
davFile.Fs = fs
|
||||
fi, err = davFile.Stat()
|
||||
if assert.NoError(t, err) {
|
||||
ctype, err := fi.(*webDavFileInfo).ContentType(ctx)
|
||||
assert.EqualError(t, err, webdav.ErrNotImplemented.Error(), "unexpected content type %q", ctype)
|
||||
}
|
||||
cache := mimeCache{
|
||||
maxSize: 10,
|
||||
mimeTypes: map[string]string{},
|
||||
}
|
||||
cache.addMimeToCache("", "")
|
||||
cache.RLock()
|
||||
assert.Len(t, cache.mimeTypes, 0)
|
||||
cache.RUnlock()
|
||||
|
||||
err = os.Remove(testFilePath)
|
||||
assert.NoError(t, err)
|
||||
|
@ -750,7 +796,7 @@ func TestTransferReadWriteErrors(t *testing.T) {
|
|||
|
||||
r, w, err = pipeat.Pipe()
|
||||
assert.NoError(t, err)
|
||||
mockFs := newMockOsFs(false, fs.ConnectionID(), user.HomeDir, r)
|
||||
mockFs := newMockOsFs(false, fs.ConnectionID(), user.HomeDir, r, nil)
|
||||
baseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile,
|
||||
common.TransferDownload, 0, 0, 0, 0, false, mockFs, dataprovider.TransferQuota{})
|
||||
davFile = newWebDavFile(baseTransfer, nil, nil)
|
||||
|
@ -790,7 +836,7 @@ func TestTransferSeek(t *testing.T) {
|
|||
}
|
||||
user.Permissions = make(map[string][]string)
|
||||
user.Permissions["/"] = []string{dataprovider.PermAny}
|
||||
fs := vfs.NewOsFs("connID", user.HomeDir, "")
|
||||
fs := newMockOsFs(true, "connID", user.HomeDir, nil, nil)
|
||||
connection := &Connection{
|
||||
BaseConnection: common.NewBaseConnection(fs.ConnectionID(), common.ProtocolWebDAV, "", "", user),
|
||||
}
|
||||
|
@ -831,6 +877,8 @@ func TestTransferSeek(t *testing.T) {
|
|||
res, err := davFile.Seek(0, io.SeekStart)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(0), res)
|
||||
err = davFile.Close()
|
||||
assert.NoError(t, err)
|
||||
davFile.Connection.RemoveTransfer(davFile.BaseTransfer)
|
||||
|
||||
davFile = newWebDavFile(baseTransfer, nil, nil)
|
||||
|
@ -838,7 +886,9 @@ func TestTransferSeek(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(len(testFileContents)), res)
|
||||
err = davFile.updateStatInfo()
|
||||
assert.Nil(t, err)
|
||||
assert.NoError(t, err)
|
||||
err = davFile.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
baseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath+"1", testFilePath+"1", testFile,
|
||||
common.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{AllowedTotalSize: 100})
|
||||
|
@ -847,26 +897,42 @@ func TestTransferSeek(t *testing.T) {
|
|||
assert.True(t, fs.IsNotExist(err))
|
||||
davFile.Connection.RemoveTransfer(davFile.BaseTransfer)
|
||||
|
||||
fs = vfs.NewOsFs(fs.ConnectionID(), user.GetHomeDir(), "")
|
||||
baseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath+"1", testFilePath+"1", testFile,
|
||||
common.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{AllowedTotalSize: 100})
|
||||
davFile = newWebDavFile(baseTransfer, nil, nil)
|
||||
_, err = davFile.Seek(0, io.SeekEnd)
|
||||
assert.True(t, fs.IsNotExist(err))
|
||||
davFile.Connection.RemoveTransfer(davFile.BaseTransfer)
|
||||
|
||||
baseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile,
|
||||
common.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{AllowedTotalSize: 100})
|
||||
davFile = newWebDavFile(baseTransfer, nil, nil)
|
||||
davFile.reader = f
|
||||
davFile.Fs = newMockOsFs(true, fs.ConnectionID(), user.GetHomeDir(), nil)
|
||||
r, _, err := pipeat.Pipe()
|
||||
assert.NoError(t, err)
|
||||
davFile.Fs = newMockOsFs(true, fs.ConnectionID(), user.GetHomeDir(), r, nil)
|
||||
res, err = davFile.Seek(2, io.SeekStart)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(2), res)
|
||||
err = davFile.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
r, _, err = pipeat.Pipe()
|
||||
assert.NoError(t, err)
|
||||
davFile = newWebDavFile(baseTransfer, nil, nil)
|
||||
davFile.Fs = newMockOsFs(true, fs.ConnectionID(), user.GetHomeDir(), nil)
|
||||
davFile.Fs = newMockOsFs(true, fs.ConnectionID(), user.GetHomeDir(), r, nil)
|
||||
res, err = davFile.Seek(2, io.SeekEnd)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(5), res)
|
||||
err = davFile.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
baseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath+"1", testFilePath+"1", testFile,
|
||||
common.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{AllowedTotalSize: 100})
|
||||
|
||||
davFile = newWebDavFile(baseTransfer, nil, nil)
|
||||
davFile.Fs = newMockOsFs(true, fs.ConnectionID(), user.GetHomeDir(), nil)
|
||||
davFile.Fs = newMockOsFs(true, fs.ConnectionID(), user.GetHomeDir(), nil, nil)
|
||||
res, err = davFile.Seek(2, io.SeekEnd)
|
||||
assert.True(t, fs.IsNotExist(err))
|
||||
assert.Equal(t, int64(0), res)
|
||||
|
|
|
@ -28,10 +28,10 @@ import (
|
|||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
"github.com/drakkan/webdav"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/rs/cors"
|
||||
"github.com/rs/xid"
|
||||
"golang.org/x/net/webdav"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/common"
|
||||
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
||||
|
|
|
@ -620,7 +620,8 @@ func TestBasicHandling(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(localUser.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, common.Connections.GetStats(), 0)
|
||||
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 },
|
||||
1*time.Second, 100*time.Millisecond)
|
||||
status := webdavd.GetStatus()
|
||||
assert.True(t, status.IsActive)
|
||||
}
|
||||
|
@ -701,7 +702,8 @@ func TestBasicHandlingCryptFs(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, common.Connections.GetStats(), 0)
|
||||
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 },
|
||||
1*time.Second, 100*time.Millisecond)
|
||||
}
|
||||
|
||||
func TestLoginEmptyPassword(t *testing.T) {
|
||||
|
@ -922,12 +924,13 @@ func TestPropPatch(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, common.Connections.GetStats(), 0)
|
||||
}
|
||||
_, err = httpdtest.RemoveUser(localUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(localUser.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 },
|
||||
1*time.Second, 100*time.Millisecond)
|
||||
}
|
||||
|
||||
func TestLoginInvalidPwd(t *testing.T) {
|
||||
|
@ -1308,7 +1311,8 @@ func TestPreDownloadHook(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, common.Connections.GetStats(), 0)
|
||||
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 },
|
||||
1*time.Second, 100*time.Millisecond)
|
||||
|
||||
common.Config.Actions.ExecuteOn = []string{common.OperationPreDownload}
|
||||
common.Config.Actions.Hook = preDownloadPath
|
||||
|
@ -1357,7 +1361,8 @@ func TestPreUploadHook(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, common.Connections.GetStats(), 0)
|
||||
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 },
|
||||
1*time.Second, 100*time.Millisecond)
|
||||
|
||||
common.Config.Actions.ExecuteOn = oldExecuteOn
|
||||
common.Config.Actions.Hook = oldHook
|
||||
|
@ -1419,7 +1424,8 @@ func TestMaxConnections(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, common.Connections.GetStats(), 0)
|
||||
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 },
|
||||
1*time.Second, 100*time.Millisecond)
|
||||
|
||||
common.Config.MaxTotalConnections = oldValue
|
||||
}
|
||||
|
@ -1450,7 +1456,8 @@ func TestMaxPerHostConnections(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, common.Connections.GetStats(), 0)
|
||||
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 },
|
||||
1*time.Second, 100*time.Millisecond)
|
||||
|
||||
common.Config.MaxPerHostConnections = oldValue
|
||||
}
|
||||
|
@ -1475,7 +1482,8 @@ func TestMaxSessions(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, common.Connections.GetStats(), 0)
|
||||
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 },
|
||||
1*time.Second, 100*time.Millisecond)
|
||||
}
|
||||
|
||||
func TestLoginWithIPilters(t *testing.T) {
|
||||
|
@ -1898,6 +1906,11 @@ func TestClientClose(t *testing.T) {
|
|||
common.Connections.Close(stat.ConnectionID)
|
||||
}
|
||||
wg.Wait()
|
||||
// for the sftp user a stat is done after the failed upload and
|
||||
// this triggers a new connection
|
||||
for _, stat := range common.Connections.GetStats() {
|
||||
common.Connections.Close(stat.ConnectionID)
|
||||
}
|
||||
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 },
|
||||
1*time.Second, 100*time.Millisecond)
|
||||
|
||||
|
@ -2104,6 +2117,19 @@ func TestBytesRangeRequests(t *testing.T) {
|
|||
assert.Equal(t, "file", string(bodyBytes))
|
||||
}
|
||||
}
|
||||
// seek on a missing file
|
||||
remotePath = fmt.Sprintf("http://%v/%v", webDavServerAddr, testFileName+"_missing")
|
||||
req, err = http.NewRequest(http.MethodGet, remotePath, nil)
|
||||
if assert.NoError(t, err) {
|
||||
httpClient := httpclient.GetHTTPClient()
|
||||
req.SetBasicAuth(user.Username, defaultPassword)
|
||||
req.Header.Set("Range", "bytes=5-")
|
||||
resp, err := httpClient.Do(req)
|
||||
if assert.NoError(t, err) {
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
err = os.Remove(testFilePath)
|
||||
assert.NoError(t, err)
|
||||
|
@ -2326,28 +2352,24 @@ func TestOsErrors(t *testing.T) {
|
|||
info, err := client.Stat(vdir)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, info.IsDir())
|
||||
// now remove the folder mapped to vdir. It should not appear in directory listing
|
||||
// now remove the folder mapped to vdir. It still appear in directory listing
|
||||
// virtual folders are automatically added
|
||||
err = os.RemoveAll(mappedPath)
|
||||
assert.NoError(t, err)
|
||||
files, err = client.ReadDir(".")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, files, 0)
|
||||
assert.Len(t, files, 1)
|
||||
err = createTestFile(filepath.Join(user.GetHomeDir(), testFileName), 32768)
|
||||
assert.NoError(t, err)
|
||||
files, err = client.ReadDir(".")
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, files, 1) {
|
||||
assert.Equal(t, testFileName, files[0].Name())
|
||||
}
|
||||
if runtime.GOOS != osWindows {
|
||||
// if the file cannot be accessed it should not appear in directory listing
|
||||
err = os.Chmod(filepath.Join(user.GetHomeDir(), testFileName), 0001)
|
||||
assert.NoError(t, err)
|
||||
files, err = client.ReadDir(".")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, files, 0)
|
||||
err = os.Chmod(filepath.Join(user.GetHomeDir(), testFileName), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, files, 2) {
|
||||
var names []string
|
||||
for _, info := range files {
|
||||
names = append(names, info.Name())
|
||||
}
|
||||
assert.Contains(t, names, testFileName)
|
||||
assert.Contains(t, names, "vdir")
|
||||
}
|
||||
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
|
@ -2803,9 +2825,18 @@ func TestSFTPLoopVirtualFolders(t *testing.T) {
|
|||
|
||||
contents, err := client.ReadDir("/")
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, contents, 1) {
|
||||
assert.Equal(t, testDir, contents[0].Name())
|
||||
assert.True(t, contents[0].IsDir())
|
||||
if assert.Len(t, contents, 2) {
|
||||
expected := 0
|
||||
for _, info := range contents {
|
||||
switch info.Name() {
|
||||
case testDir, "vdir":
|
||||
assert.True(t, info.IsDir())
|
||||
expected++
|
||||
default:
|
||||
t.Errorf("unexpected file/dir %q", info.Name())
|
||||
}
|
||||
}
|
||||
assert.Equal(t, expected, 2)
|
||||
}
|
||||
|
||||
_, err = httpdtest.RemoveUser(user1, http.StatusOK)
|
||||
|
@ -2922,7 +2953,8 @@ func TestNestedVirtualFolders(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(localUser.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, common.Connections.GetStats(), 0)
|
||||
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 },
|
||||
1*time.Second, 100*time.Millisecond)
|
||||
}
|
||||
|
||||
func checkBasicFunc(client *gowebdav.Client) error {
|
||||
|
|
|
@ -27,7 +27,7 @@ info:
|
|||
SFTPGo supports groups to simplify the administration of multiple accounts by letting you assign settings once to a group, instead of multiple times to each individual user.
|
||||
The SFTPGo WebClient allows end users to change their credentials, browse and manage their files in the browser and setup two-factor authentication which works with Authy, Google Authenticator and other compatible apps.
|
||||
From the WebClient each authorized user can also create HTTP/S links to externally share files and folders securely, by setting limits to the number of downloads/uploads, protecting the share with a password, limiting access by source IP address, setting an automatic expiration date.
|
||||
version: 2.4.0-dev
|
||||
version: 2.4.2
|
||||
contact:
|
||||
name: API support
|
||||
url: 'https://github.com/drakkan/sftpgo'
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/bash
|
||||
|
||||
NFPM_VERSION=2.20.0
|
||||
NFPM_VERSION=2.22.1
|
||||
NFPM_ARCH=${NFPM_ARCH:-amd64}
|
||||
if [ -z ${SFTPGO_VERSION} ]
|
||||
then
|
||||
|
@ -75,13 +75,13 @@ contents:
|
|||
dst: "/lib/systemd/system/sftpgo.service"
|
||||
|
||||
- src: "${BASE_DIR}/templates/*"
|
||||
dst: "/usr/share/sftpgo/templates/"
|
||||
dst: "/usr/share/sftpgo/templates"
|
||||
|
||||
- src: "${BASE_DIR}/static/*"
|
||||
dst: "/usr/share/sftpgo/static/"
|
||||
dst: "/usr/share/sftpgo/static"
|
||||
|
||||
- src: "${BASE_DIR}/openapi/*"
|
||||
dst: "/usr/share/sftpgo/openapi/"
|
||||
dst: "/usr/share/sftpgo/openapi"
|
||||
|
||||
- src: "./sftpgo.json"
|
||||
dst: "/etc/sftpgo/sftpgo.json"
|
||||
|
|
|
@ -84,6 +84,7 @@
|
|||
"host_keys": [],
|
||||
"host_certificates": [],
|
||||
"host_key_algorithms": [],
|
||||
"moduli": [],
|
||||
"kex_algorithms": [],
|
||||
"ciphers": [],
|
||||
"macs": [],
|
||||
|
|
8
static/vendor/filepond/filepond.min.css
vendored
Normal file
8
static/vendor/filepond/filepond.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
9
static/vendor/filepond/filepond.min.js
vendored
Normal file
9
static/vendor/filepond/filepond.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -72,7 +72,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" class="form-control form-control-user-custom"
|
||||
id="inputPassword" name="password" placeholder="New Password" required>
|
||||
id="inputPassword" name="password" placeholder="New Password" autocomplete="new-password" required>
|
||||
</div>
|
||||
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
|
||||
<button type="submit" class="btn btn-primary btn-user-custom btn-block">
|
||||
|
|
|
@ -73,7 +73,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
<div class="form-group row">
|
||||
<label for="idPassword" class="col-sm-2 col-form-label">Password</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control" id="idPassword" name="password" placeholder=""
|
||||
<input type="password" class="form-control" id="idPassword" name="password" placeholder="" autocomplete="new-password"
|
||||
{{if not .IsAdd}}aria-describedby="pwdHelpBlock" {{end}}>
|
||||
{{if not .IsAdd}}
|
||||
<small id="pwdHelpBlock" class="form-text text-muted">
|
||||
|
|
|
@ -73,11 +73,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" class="form-control form-control-user-custom" id="inputPassword"
|
||||
name="password" placeholder="Password" required>
|
||||
name="password" placeholder="Password" autocomplete="new-password" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" class="form-control form-control-user-custom" id="inputConfirmPassword"
|
||||
name="confirm_password" placeholder="Repeat password" required>
|
||||
name="confirm_password" placeholder="Repeat password" autocomplete="new-password" required>
|
||||
</div>
|
||||
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
|
||||
<button type="submit" class="btn btn-primary btn-user-custom btn-block">
|
||||
|
|
|
@ -33,21 +33,21 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
<div class="form-group row">
|
||||
<label for="idCurrentPassword" class="col-sm-2 col-form-label">Current password</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control" id="idCurrentPassword" name="current_password" required>
|
||||
<input type="password" class="form-control" id="idCurrentPassword" name="current_password" autocomplete="new-password" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idNewPassword1" class="col-sm-2 col-form-label">New password</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control" id="idNewPassword1" name="new_password1" required>
|
||||
<input type="password" class="form-control" id="idNewPassword1" name="new_password1" autocomplete="new-password" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idNewPassword2" class="col-sm-2 col-form-label">Confirm password</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control" id="idNewPassword2" name="new_password2" required>
|
||||
<input type="password" class="form-control" id="idNewPassword2" name="new_password2" autocomplete="new-password" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -97,7 +97,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
<div class="col-sm-2"></div>
|
||||
<label for="idHTTPPassword" class="col-sm-2 col-form-label">Password</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="password" class="form-control" id="idHTTPPassword" name="http_password" placeholder=""
|
||||
<input type="password" class="form-control" id="idHTTPPassword" name="http_password" placeholder="" autocomplete="new-password"
|
||||
value="{{if .Action.Options.HTTPConfig.Password.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.Action.Options.HTTPConfig.Password.GetPayload}}{{end}}">
|
||||
</div>
|
||||
</div>
|
||||
|
@ -669,6 +669,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
<p>
|
||||
<span class="shortcut"><b>{{`{{VirtualPath}}`}}</b></span> => Path seen by SFTPGo users, for example "/adir/afile.txt".
|
||||
</p>
|
||||
<p>
|
||||
<span class="shortcut"><b>{{`{{VirtualDirPath}}`}}</b></span> => Parent directory for VirtualPath, for example if VirtualPath is "/adir/afile.txt", VirtualDirPath is "/adir".
|
||||
</p>
|
||||
<p>
|
||||
<span class="shortcut"><b>{{`{{FsPath}}`}}</b></span> => Full filesystem path, for example "/user/homedir/adir/afile.txt" or "C:/data/user/homedir/adir/afile.txt" on Windows.
|
||||
</p>
|
||||
|
@ -681,6 +684,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
<p>
|
||||
<span class="shortcut"><b>{{`{{VirtualTargetPath}}`}}</b></span> => Virtual target path for renames.
|
||||
</p>
|
||||
<p>
|
||||
<span class="shortcut"><b>{{`{{VirtualTargetDirPath}}`}}</b></span> => Parent directory for VirtualTargetPath.
|
||||
</p>
|
||||
<p>
|
||||
<span class="shortcut"><b>{{`{{TargetName}}`}}</b></span> => Target object name for renames.
|
||||
</p>
|
||||
<p>
|
||||
<span class="shortcut"><b>{{`{{FsTargetPath}}`}}</b></span> => Full filesystem target path for renames.
|
||||
</p>
|
||||
|
|
|
@ -76,7 +76,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
<div class="col-sm-2"></div>
|
||||
<label for="idS3AccessSecret" class="col-sm-2 col-form-label">Access Secret</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="password" class="form-control" id="idS3AccessSecret" name="s3_access_secret" placeholder=""
|
||||
<input type="password" class="form-control" id="idS3AccessSecret" name="s3_access_secret" placeholder="" autocomplete="new-password"
|
||||
value="{{if .S3Config.AccessSecret.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.S3Config.AccessSecret.GetPayload}}{{end}}">
|
||||
</div>
|
||||
</div>
|
||||
|
@ -285,7 +285,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
<div class="form-group row fsconfig fsconfig-azblobfs">
|
||||
<label for="idAzAccountKey" class="col-sm-2 col-form-label">Account Key</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control" id="idAzAccountKey" name="az_account_key" placeholder=""
|
||||
<input type="password" class="form-control" id="idAzAccountKey" name="az_account_key" placeholder="" autocomplete="new-password"
|
||||
value="{{if .AzBlobConfig.AccountKey.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.AzBlobConfig.AccountKey.GetPayload}}{{end}}">
|
||||
</div>
|
||||
</div>
|
||||
|
@ -294,7 +294,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
<label for="idAzSASURL" class="col-sm-2 col-form-label">SAS URL</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control" id="idAzSASURL" name="az_sas_url" placeholder="" aria-describedby="AzSASURLHelpBlock"
|
||||
value="{{if .AzBlobConfig.SASURL.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.AzBlobConfig.SASURL.GetPayload}}{{end}}">
|
||||
autocomplete="new-password" value="{{if .AzBlobConfig.SASURL.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.AzBlobConfig.SASURL.GetPayload}}{{end}}">
|
||||
<small id="AzSASURLHelpBlock" class="form-text text-muted">
|
||||
Shared Access Signature URL can be used instead of account name/key
|
||||
</small>
|
||||
|
@ -389,7 +389,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
<label for="idCryptPassphrase" class="col-sm-2 col-form-label">Passphrase</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control" id="idCryptPassphrase" name="crypt_passphrase"
|
||||
placeholder="" aria-describedby="CryptPassphraseHelpBlock"
|
||||
placeholder="" autocomplete="new-password" aria-describedby="CryptPassphraseHelpBlock"
|
||||
value="{{if .CryptConfig.Passphrase.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.CryptConfig.Passphrase.GetPayload}}{{end}}">
|
||||
<small id="CryptPassphraseHelpBlock" class="form-text text-muted">
|
||||
Passphrase to derive the per-object encryption key
|
||||
|
@ -426,7 +426,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
<div class="col-sm-2"></div>
|
||||
<label for="idSFTPPassword" class="col-sm-2 col-form-label">Password</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="password" class="form-control" id="idSFTPPassword" name="sftp_password" placeholder=""
|
||||
<input type="password" class="form-control" id="idSFTPPassword" name="sftp_password" placeholder="" autocomplete="new-password"
|
||||
value="{{if .SFTPConfig.Password.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.SFTPConfig.Password.GetPayload}}{{end}}">
|
||||
</div>
|
||||
</div>
|
||||
|
@ -442,7 +442,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
<div class="form-group row fsconfig fsconfig-sftpfs">
|
||||
<label for="idSFTPPassphrase" class="col-sm-2 col-form-label">Key Passphrase</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control" id="idSFTPPassphrase" name="sftp_key_passphrase" placeholder=""
|
||||
<input type="password" class="form-control" id="idSFTPPassphrase" name="sftp_key_passphrase" autocomplete="new-password" placeholder=""
|
||||
value="{{if .SFTPConfig.KeyPassphrase.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.SFTPConfig.KeyPassphrase.GetPayload}}{{end}}"
|
||||
aria-describedby="SFTPPassphraseHelpBlock">
|
||||
<small id="SFTPPassphraseHelpBlock" class="form-text text-muted">
|
||||
|
@ -509,7 +509,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
<div class="col-sm-2"></div>
|
||||
<label for="idHTTPPassword" class="col-sm-2 col-form-label">Password</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="password" class="form-control" id="idHTTPPassword" name="http_password" placeholder=""
|
||||
<input type="password" class="form-control" id="idHTTPPassword" name="http_password" autocomplete="new-password" placeholder=""
|
||||
value="{{if .HTTPConfig.Password.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.HTTPConfig.Password.GetPayload}}{{end}}">
|
||||
</div>
|
||||
</div>
|
||||
|
@ -517,7 +517,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
<div class="form-group row fsconfig fsconfig-httpfs">
|
||||
<label for="idHTTPAPIKey" class="col-sm-2 col-form-label">API Key</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control" id="idHTTPAPIKey" name="http_api_key" placeholder=""
|
||||
<input type="password" class="form-control" id="idHTTPAPIKey" name="http_api_key" autocomplete="new-password" placeholder=""
|
||||
value="{{if .HTTPConfig.APIKey.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.HTTPConfig.APIKey.GetPayload}}{{end}}">
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -26,7 +26,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
<div class="card-body text-form-error">{{.Error}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<form id="login_form" action="{{.CurrentURL}}" method="POST" autocomplete="off"
|
||||
<form id="login_form" action="{{.CurrentURL}}" method="POST"
|
||||
class="user-custom">
|
||||
{{if not .FormDisabled}}
|
||||
<div class="form-group">
|
||||
|
@ -35,7 +35,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" class="form-control form-control-user-custom"
|
||||
id="inputPassword" name="password" placeholder="Password" required>
|
||||
id="inputPassword" name="password" placeholder="Password" autocomplete="current-password" required>
|
||||
{{if .ForgotPwdURL}}
|
||||
<div class="text-right">
|
||||
<a class="small" href="{{.ForgotPwdURL}}">Forgot password?</a>
|
||||
|
|
|
@ -71,7 +71,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
<input type="text" class="form-control" id="idTplUsername0" name="tpl_username" placeholder="Username" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<input type="password" class="form-control" id="idTplPassword0" name="tpl_password" placeholder="Password" maxlength="255">
|
||||
<input type="password" class="form-control" id="idTplPassword0" name="tpl_password" placeholder="Password" autocomplete="new-password">
|
||||
</div>
|
||||
<div class="form-group col-md-5">
|
||||
<textarea class="form-control" id="idTplPublicKey0" name="tpl_public_keys" rows="5"
|
||||
|
@ -108,7 +108,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
<div class="form-group row">
|
||||
<label for="idPassword" class="col-sm-2 col-form-label">Password</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control" id="idPassword" name="password" value="{{.User.Password}}" placeholder="">
|
||||
<input type="password" class="form-control" id="idPassword" name="password" value="{{.User.Password}}" placeholder="" autocomplete="new-password">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -1057,6 +1057,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
showClear: false,
|
||||
showClose: true,
|
||||
showToday: false
|
||||
},
|
||||
widgetPositioning: {
|
||||
horizontal: 'auto',
|
||||
vertical: 'bottom'
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1120,7 +1124,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
<input type="text" class="form-control" id="idTplUsername${index}" name="tpl_username" placeholder="Username" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<input type="password" class="form-control" id="idTplPassword${index}" name="tpl_password" placeholder="Password" maxlength="255">
|
||||
<input type="password" class="form-control" id="idTplPassword${index}" name="tpl_password" placeholder="Password" autocomplete="new-password">
|
||||
</div>
|
||||
<div class="form-group col-md-5">
|
||||
<textarea class="form-control" id="idTplPublicKey${index}" name="tpl_public_keys" rows="5"
|
||||
|
|
|
@ -33,21 +33,21 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
<div class="form-group row">
|
||||
<label for="idCurrentPassword" class="col-sm-2 col-form-label">Current password</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control" id="idCurrentPassword" name="current_password" required>
|
||||
<input type="password" class="form-control" id="idCurrentPassword" name="current_password" autocomplete="new-password" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idNewPassword1" class="col-sm-2 col-form-label">New password</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control" id="idNewPassword1" name="new_password1" required>
|
||||
<input type="password" class="form-control" id="idNewPassword1" name="new_password1" autocomplete="new-password" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idNewPassword2" class="col-sm-2 col-form-label">Confirm password</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control" id="idNewPassword2" name="new_password2" required>
|
||||
<input type="password" class="form-control" id="idNewPassword2" name="new_password2" autocomplete="new-password" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
<link href="{{.StaticURL}}/vendor/datatables/dataTables.checkboxes.css" rel="stylesheet">
|
||||
<link href="{{.StaticURL}}/vendor/lightbox2/css/lightbox.min.css" rel="stylesheet">
|
||||
<link href="{{.StaticURL}}/vendor/video-js/video-js.min.css" rel="stylesheet" />
|
||||
<link href="{{.StaticURL}}/vendor/filepond/filepond.min.css" rel="stylesheet" />
|
||||
<style>
|
||||
div.dataTables_wrapper span.selected-info,
|
||||
div.dataTables_wrapper span.selected-item {
|
||||
|
@ -48,7 +49,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
<div class="card-body text-form-error">{{.Error}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="table-responsive">
|
||||
<div id="tableContainer" class="table-responsive">
|
||||
<table class="table table-hover nowrap" id="dataTable" width="100%" cellspacing="0">
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -110,7 +111,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
</div>
|
||||
<form id="upload_files_form" action="{{.FilesURL}}?path={{.CurrentDir}}" method="POST" enctype="multipart/form-data">
|
||||
<div class="modal-body">
|
||||
<input type="file" class="form-control-file" id="files_name" name="filenames" required multiple>
|
||||
<div id="uploadErrorMsg" class="card mb-4 border-left-warning" style="display: none;">
|
||||
<div id="uploadErrorTxt" class="card-body text-form-error"></div>
|
||||
</div>
|
||||
<input type="file" id="files_name" name="filenames" required multiple>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<input type="hidden" name="_form_token" value="{{.CSRFToken}}">
|
||||
|
@ -229,6 +233,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
<script src="{{.StaticURL}}/vendor/codemirror/codemirror.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/codemirror/meta.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/video-js/video.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/filepond/filepond.min.js"></script>
|
||||
{{if .HasIntegrations}}
|
||||
<script type="text/javascript">
|
||||
var childReference = null;
|
||||
|
@ -530,6 +535,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
return "far fa-file-code";
|
||||
case "zip":
|
||||
case "zipx":
|
||||
case "7z":
|
||||
case "rar":
|
||||
case "tar":
|
||||
case "gz":
|
||||
|
@ -652,6 +658,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
});
|
||||
}
|
||||
|
||||
const isDirectoryEntry = item => isEntry(item) && (getAsEntry(item) || {}).isDirectory;
|
||||
const isEntry = item => 'webkitGetAsEntry' in item;
|
||||
const getAsEntry = item => item.webkitGetAsEntry();
|
||||
|
||||
$(document).ready(function () {
|
||||
player = videojs('video_player', {
|
||||
controls: true,
|
||||
|
@ -671,6 +681,72 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
$('#spinnerModal').modal('hide');
|
||||
}
|
||||
});
|
||||
|
||||
{{if .CanAddFiles}}
|
||||
FilePond.create(document.getElementById("files_name"),{
|
||||
allowMultiple: true,
|
||||
name: 'filenames',
|
||||
maxFiles: 30,
|
||||
credits: false,
|
||||
required: true,
|
||||
onwarning: function(error){
|
||||
if (error.code == 0){
|
||||
$('#uploadErrorTxt').text('You can upload a maximum of 30 files');
|
||||
$('#uploadErrorMsg').show();
|
||||
setTimeout(function () {
|
||||
$('#uploadErrorMsg').hide();
|
||||
}, 10000);
|
||||
}
|
||||
},
|
||||
beforeAddFile: (fileItem) => new Promise(resolve => {
|
||||
let num = 0;
|
||||
FilePond.find(document.getElementById("files_name")).getFiles().forEach(function(val){
|
||||
if (val.filename == fileItem.filename){
|
||||
num++;
|
||||
}
|
||||
});
|
||||
resolve(num == 1);
|
||||
})
|
||||
});
|
||||
|
||||
$('#tableContainer').on("dragover", function(ev){
|
||||
ev.preventDefault();
|
||||
$('#tableContainer').css('opacity','0.5');
|
||||
});
|
||||
|
||||
$('#tableContainer').on("dragend dragleave", function(ev){
|
||||
ev.preventDefault();
|
||||
$('#tableContainer').css('opacity','1');
|
||||
});
|
||||
|
||||
$('#tableContainer').on("drop", function(ev){
|
||||
ev.preventDefault();
|
||||
$('#tableContainer').css('opacity','1');
|
||||
let filesDropped = false;
|
||||
|
||||
if (ev.originalEvent.dataTransfer.items) {
|
||||
[...ev.originalEvent.dataTransfer.items].forEach((item, i) => {
|
||||
if (item.kind === 'file') {
|
||||
// if this is a directory just open the upload dialog
|
||||
if (!isDirectoryEntry(item)){
|
||||
FilePond.find(document.getElementById("files_name")).addFile(item.getAsFile());
|
||||
}
|
||||
filesDropped = true;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
[...ev.originalEvent.dataTransfer.files].forEach((file, i) => {
|
||||
FilePond.find(document.getElementById("files_name")).addFile(file);
|
||||
filesDropped = true;
|
||||
});
|
||||
}
|
||||
|
||||
if (filesDropped && !$('#uploadFilesModal').hasClass('show')){
|
||||
$('#uploadFilesModal').modal('show');
|
||||
}
|
||||
});
|
||||
{{end}}
|
||||
|
||||
$("#create_dir_form").submit(function (event) {
|
||||
event.preventDefault();
|
||||
$('#createDirModal').modal('hide');
|
||||
|
@ -712,7 +788,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
keepAlive();
|
||||
var keepAliveTimer = setInterval(keepAlive, 300000);
|
||||
|
||||
var files = $("#files_name")[0].files;
|
||||
var files = FilePond.find(document.getElementById("files_name")).getFiles();
|
||||
var has_errors = false;
|
||||
var index = 0;
|
||||
var success = 0;
|
||||
|
@ -738,7 +814,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
var errorMessage = "Error uploading files";
|
||||
let response;
|
||||
try {
|
||||
var f = files[index];
|
||||
var f = files[index].file;
|
||||
var uploadPath = '{{.FileURL}}?path={{.CurrentDir}}'+encodeURIComponent("/"+f.name);
|
||||
var lastModified;
|
||||
try {
|
||||
|
@ -887,7 +963,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
name: 'addFiles',
|
||||
titleAttr: "Upload files",
|
||||
action: function (e, dt, node, config) {
|
||||
document.getElementById("files_name").value = null;
|
||||
//FilePond.find(document.getElementById("files_name")).removeFiles();
|
||||
$('#uploadFilesModal').modal('show');
|
||||
},
|
||||
enabled: true
|
||||
|
|
|
@ -23,7 +23,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
<div class="card-body text-form-error">{{.Error}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<form id="login_form" action="{{.CurrentURL}}" method="POST" autocomplete="off"
|
||||
<form id="login_form" action="{{.CurrentURL}}" method="POST"
|
||||
class="user-custom">
|
||||
{{if not .FormDisabled}}
|
||||
<div class="form-group">
|
||||
|
@ -32,7 +32,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" class="form-control form-control-user-custom"
|
||||
id="inputPassword" name="password" placeholder="Password" required>
|
||||
id="inputPassword" name="password" placeholder="Password" autocomplete="current-password" required>
|
||||
{{if .ForgotPwdURL}}
|
||||
<div class="text-right">
|
||||
<a class="small" href="{{.ForgotPwdURL}}">Forgot password?</a>
|
||||
|
|
|
@ -102,7 +102,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
<div class="form-group row">
|
||||
<label for="idPassword" class="col-sm-2 col-form-label">Password</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control" id="idPassword" name="password" placeholder=""
|
||||
<input type="password" class="form-control" id="idPassword" name="password" autocomplete="new-password" placeholder=""
|
||||
value="{{.Share.Password}}" aria-describedby="passwordHelpBlock">
|
||||
<small id="passwordHelpBlock" class="form-text text-muted">
|
||||
If set the share will be password-protected
|
||||
|
|
|
@ -23,6 +23,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
<link href="{{.StaticURL}}/vendor/datatables/fixedHeader.bootstrap4.min.css" rel="stylesheet">
|
||||
<link href="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.css" rel="stylesheet">
|
||||
<link href="{{.StaticURL}}/vendor/datatables/dataTables.checkboxes.css" rel="stylesheet">
|
||||
<link href="{{.StaticURL}}/vendor/filepond/filepond.min.css" rel="stylesheet" />
|
||||
<style>
|
||||
div.dataTables_wrapper span.selected-info,
|
||||
div.dataTables_wrapper span.selected-item {
|
||||
|
@ -45,7 +46,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
<div id="errorMsg" class="card mb-4 border-left-warning" style="display: none;">
|
||||
<div id="errorTxt" class="card-body text-form-error"></div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<div id="tableContainer" class="table-responsive">
|
||||
<table class="table table-hover nowrap" id="dataTable" width="100%" cellspacing="0">
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -76,7 +77,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
</div>
|
||||
<form id="upload_files_form" action="{{.FilesURL}}?path={{.CurrentDir}}" method="POST" enctype="multipart/form-data">
|
||||
<div class="modal-body">
|
||||
<input type="file" class="form-control-file" id="files_name" name="filenames" required multiple>
|
||||
<div id="uploadErrorMsg" class="card mb-4 border-left-warning" style="display: none;">
|
||||
<div id="uploadErrorTxt" class="card-body text-form-error"></div>
|
||||
</div>
|
||||
<input type="file" id="files_name" name="filenames" required multiple>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" type="button" data-dismiss="modal">Cancel</button>
|
||||
|
@ -102,6 +106,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
<script src="{{.StaticURL}}/vendor/datatables/dataTables.responsive.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/datatables/dataTables.checkboxes.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/filepond/filepond.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
var spinnerDone = false;
|
||||
|
||||
|
@ -191,6 +196,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
return "far fa-file-code";
|
||||
case "zip":
|
||||
case "zipx":
|
||||
case "7z":
|
||||
case "rar":
|
||||
case "tar":
|
||||
case "gz":
|
||||
|
@ -223,6 +229,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
return meta.split('_').slice(1).join('_');
|
||||
}
|
||||
|
||||
const isDirectoryEntry = item => isEntry(item) && (getAsEntry(item) || {}).isDirectory;
|
||||
const isEntry = item => 'webkitGetAsEntry' in item;
|
||||
const getAsEntry = item => item.webkitGetAsEntry();
|
||||
|
||||
$(document).ready(function () {
|
||||
$('#spinnerModal').on('shown.bs.modal', function () {
|
||||
if (spinnerDone){
|
||||
|
@ -230,9 +240,74 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
}
|
||||
});
|
||||
|
||||
{{if gt .Scope 1}}
|
||||
FilePond.create(document.getElementById("files_name"),{
|
||||
allowMultiple: true,
|
||||
name: 'filenames',
|
||||
maxFiles: 30,
|
||||
credits: false,
|
||||
required: true,
|
||||
onwarning: function(error){
|
||||
if (error.code == 0){
|
||||
$('#uploadErrorTxt').text('You can upload a maximum of 30 files');
|
||||
$('#uploadErrorMsg').show();
|
||||
setTimeout(function () {
|
||||
$('#uploadErrorMsg').hide();
|
||||
}, 10000);
|
||||
}
|
||||
},
|
||||
beforeAddFile: (fileItem) => new Promise(resolve => {
|
||||
let num = 0;
|
||||
FilePond.find(document.getElementById("files_name")).getFiles().forEach(function(val){
|
||||
if (val.filename == fileItem.filename){
|
||||
num++;
|
||||
}
|
||||
});
|
||||
resolve(num == 1);
|
||||
})
|
||||
});
|
||||
|
||||
$('#tableContainer').on("dragover", function(ev){
|
||||
ev.preventDefault();
|
||||
$('#tableContainer').css('opacity','0.5');
|
||||
});
|
||||
|
||||
$('#tableContainer').on("dragend dragleave", function(ev){
|
||||
ev.preventDefault();
|
||||
$('#tableContainer').css('opacity','1');
|
||||
});
|
||||
|
||||
$('#tableContainer').on("drop", function(ev){
|
||||
ev.preventDefault();
|
||||
$('#tableContainer').css('opacity','1');
|
||||
let filesDropped = false;
|
||||
|
||||
if (ev.originalEvent.dataTransfer.items) {
|
||||
[...ev.originalEvent.dataTransfer.items].forEach((item, i) => {
|
||||
if (item.kind === 'file') {
|
||||
// if this is a directory just open the upload dialog
|
||||
if (!isDirectoryEntry(item)){
|
||||
FilePond.find(document.getElementById("files_name")).addFile(item.getAsFile());
|
||||
filesDropped = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
[...ev.originalEvent.dataTransfer.files].forEach((file, i) => {
|
||||
FilePond.find(document.getElementById("files_name")).addFile(file);
|
||||
filesDropped = true;
|
||||
});
|
||||
}
|
||||
|
||||
if (filesDropped && !$('#uploadFilesModal').hasClass('show')){
|
||||
$('#uploadFilesModal').modal('show');
|
||||
}
|
||||
});
|
||||
{{end}}
|
||||
|
||||
$("#upload_files_form").submit(function (event){
|
||||
event.preventDefault();
|
||||
var files = $("#files_name")[0].files;
|
||||
var files = FilePond.find(document.getElementById("files_name")).getFiles();
|
||||
var has_errors = false;
|
||||
var index = 0;
|
||||
var success = 0;
|
||||
|
@ -255,7 +330,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
var errorMessage = "Error uploading files";
|
||||
let response;
|
||||
try {
|
||||
var f = files[index];
|
||||
var f = files[index].file;
|
||||
var uploadPath = '{{.UploadBaseURL}}'+fixedEncodeURIComponent("/"+escapeHTML(f.name));
|
||||
var lastModified;
|
||||
try {
|
||||
|
@ -345,7 +420,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
name: 'addFiles',
|
||||
titleAttr: "Upload files",
|
||||
action: function (e, dt, node, config) {
|
||||
document.getElementById("files_name").value = null;
|
||||
//FilePond.find(document.getElementById("files_name")).removeFiles();
|
||||
$('#uploadFilesModal').modal('show');
|
||||
},
|
||||
enabled: true
|
||||
|
|
Loading…
Add table
Reference in a new issue