44 Commits

Author SHA1 Message Date
dependabot[bot]
2c2aa379fd Bump github.com/stretchr/testify from 1.8.2 to 1.8.3 (#330)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.2 to 1.8.3.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.8.2...v1.8.3)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-23 17:29:40 -04:00
dependabot[bot]
f5852338bb Bump actions/setup-go from 4.0.0 to 4.0.1 (#328)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](4d34df0c23...fac708d667)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-23 17:29:21 -04:00
Nick Craig-Wood
36e873b513 Fix GetEntry / MLST for some servers - Fixes #321 (#322)
Some servers seem to send a blank line at the end of an MLST response.

    MLST Download
    250-
    Size=53248;Modify=20230327134339.000;Type=dir; Download

    250 Requested file action okay, completed.

Before this change this would cause the GetEntry method to return this
error.

    unsupported LIST line

This patch ignores blank lines in the MLST response.
2023-05-12 22:29:37 -04:00
dependabot[bot]
0feadd74ba Bump github.com/stretchr/testify from 1.8.1 to 1.8.2 (#311)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.1 to 1.8.2.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.8.1...v1.8.2)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-12 22:27:57 -04:00
dependabot[bot]
67ffbbc0de Bump actions/cache from 3.2.5 to 3.3.1 (#318)
Bumps [actions/cache](https://github.com/actions/cache) from 3.2.5 to 3.3.1.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](6998d139dd...88522ab9f3)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-12 22:27:45 -04:00
dependabot[bot]
3eddb768d0 Bump actions/setup-go from 3.5.0 to 4.0.0 (#319)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 3.5.0 to 4.0.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](6edd4406fa...4d34df0c23)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-12 22:27:33 -04:00
dependabot[bot]
f568a0d846 Bump coverallsapp/github-action from 1.1.3 to 2.1.2 (#327)
Bumps [coverallsapp/github-action](https://github.com/coverallsapp/github-action) from 1.1.3 to 2.1.2.
- [Release notes](https://github.com/coverallsapp/github-action/releases)
- [Commits](9ba913c152...f350da2c03)

---
updated-dependencies:
- dependency-name: coverallsapp/github-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-12 22:27:20 -04:00
dependabot[bot]
d84bf4be2b Bump actions/cache from 3.2.4 to 3.2.5 (#308)
Bumps [actions/cache](https://github.com/actions/cache) from 3.2.4 to 3.2.5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](627f0f41f6...6998d139dd)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-13 19:46:52 -05:00
Nick Craig-Wood
9e39e2c406 Fix hang when using ExplicitTLS to certain servers. (#283)
In #282 it was discovered that doing the tls Handshake immediately on
connection causes some FTP servers (proftpd and pureftpd) to hang.

The exact cause of this is unknown, but this patch works around the
problem by not doing the Handsake initially, and only doing it at the
end if we were attempting to upload a zero length file.

Doing the Handshake at the end was originally added in
a4e9650823 however it got reverted in 212daf295f which
used tls.DialWithDialer to do the handshake. Unfortunately
tls.DialWithDialer seems to trigger the hanging bug.

See: https://forum.rclone.org/t/rclone-ftps-explicit-rclone-touch-empty-files-proftpd-unable-to-build-data-connection-operation-not-permitted/22522
See: https://github.com/rclone/rclone/issues/6426#issuecomment-1243993039
Fixes #282
2023-02-08 11:46:41 -05:00
dependabot[bot]
58cb524052 Bump actions/cache from 3.2.3 to 3.2.4 (#307)
Bumps [actions/cache](https://github.com/actions/cache) from 3.2.3 to 3.2.4.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](58c146cc91...627f0f41f6)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-08 11:44:47 -05:00
dependabot[bot]
0257a7bc42 Bump golangci/golangci-lint-action from 3.3.1 to 3.4.0 (#306)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 3.3.1 to 3.4.0.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](0ad9a0988b...08e2f20817)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-23 20:58:31 -05:00
dependabot[bot]
5fc8c694ec Bump actions/cache from 3.2.0 to 3.2.3 (#304)
Bumps [actions/cache](https://github.com/actions/cache) from 3.2.0 to 3.2.3.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](c17f4bf466...58c146cc91)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-16 13:19:23 -05:00
dependabot[bot]
b10ce93530 Bump github.com/stretchr/testify from 1.8.0 to 1.8.1 (#294)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.0 to 1.8.1.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.8.0...v1.8.1)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-21 17:33:01 -05:00
dependabot[bot]
5b1ec4dc6c Bump golangci/golangci-lint-action from 3.2.0 to 3.3.1 (#297)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 3.2.0 to 3.3.1.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](537aa1903e...0ad9a0988b)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-21 17:32:28 -05:00
dependabot[bot]
a0ec57882b Bump actions/setup-go from 3.3.0 to 3.5.0 (#300)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 3.3.0 to 3.5.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](268d8c0ca0...6edd4406fa)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-21 17:32:07 -05:00
dependabot[bot]
917837ffbd Bump actions/cache from 3.0.8 to 3.2.0 (#301)
Bumps [actions/cache](https://github.com/actions/cache) from 3.0.8 to 3.2.0.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](fd5de65bc8...c17f4bf466)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-21 17:31:46 -05:00
dependabot[bot]
6512c2a4ae Bump actions/setup-go from 2.2.0 to 3.3.0 (#280)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 2.2.0 to 3.3.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](bfdd3570ce...268d8c0ca0)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-07 18:52:58 -04:00
dependabot[bot]
a2b3f0878b Bump coverallsapp/github-action from 1.1.2 to 1.1.3 (#274)
Bumps [coverallsapp/github-action](https://github.com/coverallsapp/github-action) from 1.1.2 to 1.1.3.
- [Release notes](https://github.com/coverallsapp/github-action/releases)
- [Commits](https://github.com/coverallsapp/github-action/compare/v1.1.2...1.1.3)

---
updated-dependencies:
- dependency-name: coverallsapp/github-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-07 18:46:14 -04:00
dependabot[bot]
b40593bd5a Bump jandelgado/gcov2lcov-action from 1.0.8 to 1.0.9 (#273)
Bumps [jandelgado/gcov2lcov-action](https://github.com/jandelgado/gcov2lcov-action) from 1.0.8 to 1.0.9.
- [Release notes](https://github.com/jandelgado/gcov2lcov-action/releases)
- [Changelog](https://github.com/jandelgado/gcov2lcov-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jandelgado/gcov2lcov-action/compare/v1.0.8...v1.0.9)

---
updated-dependencies:
- dependency-name: jandelgado/gcov2lcov-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-07 18:45:43 -04:00
dependabot[bot]
dca029e125 Bump golangci/golangci-lint-action from 2.5.2 to 3.2.0 (#278)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 2.5.2 to 3.2.0.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](5c56cd6c9d...537aa1903e)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-07 18:45:22 -04:00
dependabot[bot]
08a9ec380c Bump actions/cache from 2.1.7 to 3.0.8 (#279)
Bumps [actions/cache](https://github.com/actions/cache) from 2.1.7 to 3.0.8.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](937d244753...fd5de65bc8)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-07 18:42:48 -04:00
Julien Laffaye
260999f2de Secure GitHub workflows 2022-09-07 18:14:30 -04:00
Julien Laffaye
f1ba13192b Add dependabot for github-actions 2022-09-07 18:03:34 -04:00
Julien Laffaye
9c3c2fd740 Add CodeQL and golangci-lint badges 2022-09-07 18:02:26 -04:00
Julien Laffaye
be12fe0263 Create codeql-analysis.yml 2022-09-07 17:48:18 -04:00
Øyvind Heddeland Instefjord
99be0634ab Add forceListHidden dial option to force the use of 'LIST -a' command (#271)
This is useful for servers that do not offer up
hidden folders/files by default when using LIST/MLSD
commands.
Setting forceListHidden to true will force the use of
the 'LIST -a' command even when MLST support has
been detected.
2022-09-04 14:43:06 -04:00
Julien Laffaye
b85cf1edcc Add default timeout to instantiate connection 2022-08-28 21:58:25 -04:00
Thomas Hallgren
0aeb8660a7 Add MLST command in the form of a Get method (#269)
* Add MLST command in the form of a Get method

The `LIST` and `MLSD` commands are inefficient when the objective
is to retrieve one single `Entry` for a known path, because the only way
to get such an entry is to list the parent directory using a data
connection. The `MLST` fixes this by allowing one single `Entry` to be
returned using the control connection.

The name `Get` was chosen because it is often used in conjunction with
`List` as a mean to get one single entry.

Signed-off-by: Thomas Hallgren <thomas@datawire.io>

* Changes in response to code review:

- Rename `Get` to `GetEntry` (because it returns an `*Entry`)
- Add test-case for multiline response on the control connection
- Fix issues with parsing the multiline response.

Signed-off-by: Thomas Hallgren <thomas@datawire.io>

* Add sample output from MLST to GetEntry comment.

Signed-off-by: Thomas Hallgren <thomas@datawire.io>

* Changes in response to code review:

- Remove unused `time.Time` argument
- Add struct labels to make `govet` happy

Signed-off-by: Thomas Hallgren <thomas@datawire.io>

* Remove time arg when calling parseNextRFC3659ListLine

Signed-off-by: Thomas Hallgren <thomas@datawire.io>

Signed-off-by: Thomas Hallgren <thomas@datawire.io>
2022-08-21 17:25:29 -04:00
Julien Laffaye
4d1d644cf1 Mark Connect() and DialWithTimeout() as deprecated 2022-08-18 12:44:22 -04:00
Julien Laffaye
5a2fd50da8 ioutil is deprecated, use io package instead 2022-08-18 11:18:54 -04:00
Julien Laffaye
9cda78131d Fix transferring spelling. 2022-08-18 10:17:32 -04:00
Julien Laffaye
39592b91e4 Assign type to TransferType constants 2022-08-17 19:43:45 -04:00
Julien Laffaye
fa83b53d0e Use tls.Dialer to be able to use DialContext 2022-08-17 19:35:55 -04:00
Julien Laffaye
45482d097e Use assert package to simplify tests 2022-08-17 19:24:40 -04:00
Julien Laffaye
560423fa8a DialWithNetConn is a special case of DialWithDialFunc 2022-08-17 18:41:24 -04:00
Julien Laffaye
11536801d1 Merge pull request #264 from jlaffaye/dependabot/go_modules/github.com/stretchr/testify-1.8.0
Bump github.com/stretchr/testify from 1.7.2 to 1.8.0
2022-06-30 12:50:35 -04:00
dependabot[bot]
6c74f63d3c Bump github.com/stretchr/testify from 1.7.2 to 1.8.0
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.7.2 to 1.8.0.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.7.2...v1.8.0)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-29 15:39:38 +00:00
Julien Laffaye
60a941566c Merge pull request #259 from jlaffaye/dependabot/go_modules/github.com/stretchr/testify-1.7.2
Bump github.com/stretchr/testify from 1.6.1 to 1.7.2
2022-06-12 11:18:34 -04:00
dependabot[bot]
712e6cb8bc Bump github.com/stretchr/testify from 1.6.1 to 1.7.2
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.6.1 to 1.7.2.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.6.1...v1.7.2)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-10 16:37:09 +00:00
Julien Laffaye
6696131620 Create dependabot.yml 2022-06-10 12:36:43 -04:00
Julien Laffaye
dfa1e758f3 Merge pull request #257 from tauu/cmd-type
support for the TYPE command
2022-05-23 20:19:17 -04:00
Georg Wechslberger
b29e1f6c62 refactor: rename TransferTypeImage to TransferTypeBinary 2022-05-23 17:21:05 +02:00
Georg Wechslberger
190f39e8b2 feat: support TYPE command 2022-05-23 12:36:17 +02:00
Julien Laffaye
d2c44e311e Restore previous behavior for List
Fixes #251
2022-03-10 15:20:11 -05:00
12 changed files with 412 additions and 182 deletions

17
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "gomod" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "daily"
assignees:
- "jlaffaye"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"

69
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -0,0 +1,69 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ "master" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "master" ]
schedule:
- cron: '20 19 * * 2'
permissions:
contents: read
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b398f525a5587552e573b247ac661067fafa920b

View File

@@ -5,9 +5,12 @@ jobs:
golangci-lint: golangci-lint:
name: lint name: lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read # for actions/checkout to fetch code
pull-requests: read # for golangci/golangci-lint-action to fetch pull requests
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@7884fcad6b5d53d10323aee724dc68d8b9096a2e
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v2 uses: golangci/golangci-lint-action@08e2f20817b15149a52b5b3ebe7de50aff2ba8c5
with: with:
only-new-issues: true only-new-issues: true

View File

@@ -5,12 +5,12 @@ jobs:
name: test name: test
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089
- name: Setup go - name: Setup go
uses: actions/setup-go@v2 uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753
with: with:
go-version: 1.17 go-version: 1.19
- uses: actions/cache@v2 - uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8
with: with:
path: | path: |
~/go/pkg/mod ~/go/pkg/mod
@@ -21,9 +21,9 @@ jobs:
- name: Run tests - name: Run tests
run: go test -v -covermode=count -coverprofile=coverage.out run: go test -v -covermode=count -coverprofile=coverage.out
- name: Convert coverage to lcov - name: Convert coverage to lcov
uses: jandelgado/gcov2lcov-action@v1.0.8 uses: jandelgado/gcov2lcov-action@c680c0f7c7442485f1749eb2a13e54a686e76eb5
- name: Coveralls - name: Coveralls
uses: coverallsapp/github-action@v1.1.2 uses: coverallsapp/github-action@f350da2c033043742f89e8c0b7b5145a1616da6d
with: with:
github-token: ${{ secrets.github_token }} github-token: ${{ secrets.github_token }}
path-to-lcov: coverage.lcov path-to-lcov: coverage.lcov

View File

@@ -2,6 +2,8 @@
[![Units tests](https://github.com/jlaffaye/ftp/actions/workflows/unit_tests.yaml/badge.svg)](https://github.com/jlaffaye/ftp/actions/workflows/unit_tests.yaml) [![Units tests](https://github.com/jlaffaye/ftp/actions/workflows/unit_tests.yaml/badge.svg)](https://github.com/jlaffaye/ftp/actions/workflows/unit_tests.yaml)
[![Coverage Status](https://coveralls.io/repos/jlaffaye/ftp/badge.svg?branch=master&service=github)](https://coveralls.io/github/jlaffaye/ftp?branch=master) [![Coverage Status](https://coveralls.io/repos/jlaffaye/ftp/badge.svg?branch=master&service=github)](https://coveralls.io/github/jlaffaye/ftp?branch=master)
[![golangci-lint](https://github.com/jlaffaye/ftp/actions/workflows/golangci-lint.yaml/badge.svg)](https://github.com/jlaffaye/ftp/actions/workflows/golangci-lint.yaml)
[![CodeQL](https://github.com/jlaffaye/ftp/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/jlaffaye/ftp/actions/workflows/codeql-analysis.yml)
[![Go ReportCard](https://goreportcard.com/badge/jlaffaye/ftp)](http://goreportcard.com/report/jlaffaye/ftp) [![Go ReportCard](https://goreportcard.com/badge/jlaffaye/ftp)](http://goreportcard.com/report/jlaffaye/ftp)
[![Go Reference](https://pkg.go.dev/badge/github.com/jlaffaye/ftp.svg)](https://pkg.go.dev/github.com/jlaffaye/ftp) [![Go Reference](https://pkg.go.dev/badge/github.com/jlaffaye/ftp.svg)](https://pkg.go.dev/github.com/jlaffaye/ftp)

View File

@@ -3,10 +3,8 @@ package ftp
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"io/ioutil" "io"
"net" "net"
"net/textproto"
"strings"
"syscall" "syscall"
"testing" "testing"
"time" "time"
@@ -28,174 +26,145 @@ func TestConnEPSV(t *testing.T) {
} }
func testConn(t *testing.T, disableEPSV bool) { func testConn(t *testing.T, disableEPSV bool) {
assert := assert.New(t)
mock, c := openConn(t, "127.0.0.1", DialWithTimeout(5*time.Second), DialWithDisabledEPSV(disableEPSV)) mock, c := openConn(t, "127.0.0.1", DialWithTimeout(5*time.Second), DialWithDisabledEPSV(disableEPSV))
err := c.Login("anonymous", "anonymous") err := c.Login("anonymous", "anonymous")
if err != nil { assert.NoError(err)
t.Fatal(err)
}
err = c.NoOp() err = c.NoOp()
if err != nil { assert.NoError(err)
t.Error(err)
}
err = c.ChangeDir("incoming") err = c.ChangeDir("incoming")
if err != nil { assert.NoError(err)
t.Error(err)
}
dir, err := c.CurrentDir() dir, err := c.CurrentDir()
if err != nil { if assert.NoError(err) {
t.Error(err) assert.Equal("/incoming", dir)
} else {
if dir != "/incoming" {
t.Error("Wrong dir: " + dir)
}
} }
data := bytes.NewBufferString(testData) data := bytes.NewBufferString(testData)
err = c.Stor("test", data) err = c.Stor("test", data)
if err != nil { assert.NoError(err)
t.Error(err)
}
_, err = c.List(".") _, err = c.List(".")
if err != nil { assert.NoError(err)
t.Error(err)
}
err = c.Rename("test", "tset") err = c.Rename("test", "tset")
if err != nil { assert.NoError(err)
t.Error(err)
}
// Read without deadline // Read without deadline
r, err := c.Retr("tset") r, err := c.Retr("tset")
if err != nil { if assert.NoError(err) {
t.Error(err) buf, err := io.ReadAll(r)
} else { if assert.NoError(err) {
buf, errRead := ioutil.ReadAll(r) assert.Equal(testData, string(buf))
if err != nil {
t.Error(errRead)
}
if string(buf) != testData {
t.Errorf("'%s'", buf)
} }
r.Close() r.Close()
r.Close() // test we can close two times r.Close() // test we can close two times
} }
// Read with deadline // Read with deadline
r, err = c.Retr("tset") r, err = c.Retr("tset")
if err != nil { if assert.NoError(err) {
t.Error(err)
} else {
if err := r.SetDeadline(time.Now()); err != nil { if err := r.SetDeadline(time.Now()); err != nil {
t.Fatal(err) t.Fatal(err)
} }
_, err = ioutil.ReadAll(r) _, err = io.ReadAll(r)
if err == nil { assert.ErrorContains(err, "i/o timeout")
t.Error("deadline should have caused error")
} else if !strings.HasSuffix(err.Error(), "i/o timeout") {
t.Error(err)
}
r.Close() r.Close()
} }
// Read with offset // Read with offset
r, err = c.RetrFrom("tset", 5) r, err = c.RetrFrom("tset", 5)
if err != nil { if assert.NoError(err) {
t.Error(err) buf, err := io.ReadAll(r)
} else { if assert.NoError(err) {
buf, errRead := ioutil.ReadAll(r)
if errRead != nil {
t.Error(errRead)
}
expected := testData[5:] expected := testData[5:]
if string(buf) != expected { assert.Equal(expected, string(buf))
t.Errorf("read %q, expected %q", buf, expected)
} }
r.Close() r.Close()
} }
data2 := bytes.NewBufferString(testData) data2 := bytes.NewBufferString(testData)
err = c.Append("tset", data2) err = c.Append("tset", data2)
if err != nil { assert.NoError(err)
t.Error(err)
}
// Read without deadline, after append // Read without deadline, after append
r, err = c.Retr("tset") r, err = c.Retr("tset")
if err != nil { if assert.NoError(err) {
t.Error(err) buf, err := io.ReadAll(r)
} else { if assert.NoError(err) {
buf, errRead := ioutil.ReadAll(r) assert.Equal(testData+testData, string(buf))
if err != nil {
t.Error(errRead)
}
if string(buf) != testData+testData {
t.Errorf("'%s'", buf)
} }
r.Close() r.Close()
} }
fileSize, err := c.FileSize("magic-file") fileSize, err := c.FileSize("magic-file")
assert.NoError(err)
assert.Equal(int64(42), fileSize)
_, err = c.FileSize("not-found")
assert.Error(err)
entry, err := c.GetEntry("magic-file")
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
if fileSize != 42 { if entry == nil {
t.Errorf("file size %q, expected %q", fileSize, 42) t.Fatal("expected entry, got nil")
}
if entry.Size != 42 {
t.Errorf("entry size %q, expected %q", entry.Size, 42)
}
if entry.Type != EntryTypeFile {
t.Errorf("entry type %q, expected %q", entry.Type, EntryTypeFile)
}
if entry.Name != "magic-file" {
t.Errorf("entry name %q, expected %q", entry.Name, "magic-file")
} }
_, err = c.FileSize("not-found") entry, err = c.GetEntry("multiline-dir")
if err == nil { if err != nil {
t.Fatal("expected error, got nil") t.Error(err)
}
if entry == nil {
t.Fatal("expected entry, got nil")
}
if entry.Size != 0 {
t.Errorf("entry size %q, expected %q", entry.Size, 0)
}
if entry.Type != EntryTypeFolder {
t.Errorf("entry type %q, expected %q", entry.Type, EntryTypeFolder)
}
if entry.Name != "multiline-dir" {
t.Errorf("entry name %q, expected %q", entry.Name, "multiline-dir")
} }
err = c.Delete("tset") err = c.Delete("tset")
if err != nil { assert.NoError(err)
t.Error(err)
}
err = c.MakeDir(testDir) err = c.MakeDir(testDir)
if err != nil { assert.NoError(err)
t.Error(err)
}
err = c.ChangeDir(testDir) err = c.ChangeDir(testDir)
if err != nil { assert.NoError(err)
t.Error(err)
}
err = c.ChangeDirToParent() err = c.ChangeDirToParent()
if err != nil { assert.NoError(err)
t.Error(err)
}
entries, err := c.NameList("/") entries, err := c.NameList("/")
if err != nil { assert.NoError(err)
t.Error(err) assert.Equal([]string{"/incoming"}, entries)
}
if len(entries) != 1 || entries[0] != "/incoming" {
t.Errorf("Unexpected entries: %v", entries)
}
err = c.RemoveDir(testDir) err = c.RemoveDir(testDir)
if err != nil { assert.NoError(err)
t.Error(err)
}
err = c.Logout() err = c.Logout()
if err != nil { assert.NoError(err)
if protoErr := err.(*textproto.Error); protoErr != nil {
if protoErr.Code != StatusNotImplemented {
t.Error(err)
}
} else {
t.Error(err)
}
}
if err = c.Quit(); err != nil { if err = c.Quit(); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -205,9 +174,7 @@ func testConn(t *testing.T, disableEPSV bool) {
mock.Wait() mock.Wait()
err = c.NoOp() err = c.NoOp()
if err == nil { assert.Error(err, "should error on closed conn")
t.Error("Expected error")
}
} }
// TestConnect tests the legacy Connect function // TestConnect tests the legacy Connect function
@@ -312,7 +279,7 @@ func TestMissingFolderDeleteDirRecur(t *testing.T) {
} }
func TestListCurrentDir(t *testing.T) { func TestListCurrentDir(t *testing.T) {
mock, c := openConn(t, "127.0.0.1") mock, c := openConnExt(t, "127.0.0.1", "no-time", DialWithDisabledMLSD(true))
_, err := c.List("") _, err := c.List("")
assert.NoError(t, err) assert.NoError(t, err)
@@ -328,6 +295,20 @@ func TestListCurrentDir(t *testing.T) {
mock.Wait() mock.Wait()
} }
func TestListCurrentDirWithForceListHidden(t *testing.T) {
mock, c := openConnExt(t, "127.0.0.1", "no-time", DialWithDisabledMLSD(true), DialWithForceListHidden(true))
assert.True(t, c.options.forceListHidden)
_, err := c.List("")
assert.NoError(t, err)
assert.Equal(t, "LIST -a", mock.lastFull, "LIST -a must not have a trailing whitespace")
err = c.Quit()
assert.NoError(t, err)
mock.Wait()
}
func TestTimeUnsupported(t *testing.T) { func TestTimeUnsupported(t *testing.T) {
mock, c := openConnExt(t, "127.0.0.1", "no-time") mock, c := openConnExt(t, "127.0.0.1", "no-time")

View File

@@ -6,12 +6,14 @@ import (
"io" "io"
"net" "net"
"net/textproto" "net/textproto"
"reflect"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
type ftpMock struct { type ftpMock struct {
@@ -88,7 +90,7 @@ func (mock *ftpMock) listen() {
// At least one command must have a multiline response // At least one command must have a multiline response
switch cmdParts[0] { switch cmdParts[0] {
case "FEAT": case "FEAT":
features := "211-Features:\r\n FEAT\r\n PASV\r\n EPSV\r\n UTF8\r\n SIZE\r\n" features := "211-Features:\r\n FEAT\r\n PASV\r\n EPSV\r\n UTF8\r\n SIZE\r\n MLST\r\n"
switch mock.modtime { switch mock.modtime {
case "std-time": case "std-time":
features += " MDTM\r\n MFMT\r\n" features += " MDTM\r\n MFMT\r\n"
@@ -173,9 +175,26 @@ func (mock *ftpMock) listen() {
mock.dataConn.Wait() mock.dataConn.Wait()
mock.printfLine("150 Opening ASCII mode data connection for file list") mock.printfLine("150 Opening ASCII mode data connection for file list")
mock.dataConn.write([]byte("-rw-r--r-- 1 ftp wheel 0 Jan 29 10:29 lo")) mock.dataConn.write([]byte("-rw-r--r-- 1 ftp wheel 0 Jan 29 10:29 lo\r\ntotal 1"))
mock.printfLine("226 Transfer complete") mock.printfLine("226 Transfer complete")
mock.closeDataConn() mock.closeDataConn()
case "MLSD":
if mock.dataConn == nil {
mock.printfLine("425 Unable to build data connection: Connection refused")
break
}
mock.dataConn.Wait()
mock.printfLine("150 Opening data connection for file list")
mock.dataConn.write([]byte("Type=file;Size=0;Modify=20201213202400; lo\r\n"))
mock.printfLine("226 Transfer complete")
mock.closeDataConn()
case "MLST":
if cmdParts[1] == "multiline-dir" {
mock.printfLine("250-File data\r\n Type=dir;Size=0; multiline-dir\r\n Modify=20201213202400; multiline-dir\r\n250 End")
} else {
mock.printfLine("250-File data\r\n Type=file;Size=42;Modify=20201213202400; magic-file\r\n \r\n250 End")
}
case "NLST": case "NLST":
if mock.dataConn == nil { if mock.dataConn == nil {
mock.printfLine("425 Unable to build data connection: Connection refused") mock.printfLine("425 Unable to build data connection: Connection refused")
@@ -386,20 +405,14 @@ func openConn(t *testing.T, addr string, options ...DialOption) (*ftpMock, *Serv
func openConnExt(t *testing.T, addr, modtime string, options ...DialOption) (*ftpMock, *ServerConn) { func openConnExt(t *testing.T, addr, modtime string, options ...DialOption) (*ftpMock, *ServerConn) {
mock, err := newFtpMockExt(t, addr, modtime) mock, err := newFtpMockExt(t, addr, modtime)
if err != nil { require.NoError(t, err)
t.Fatal(err)
}
defer mock.Close() defer mock.Close()
c, err := Dial(mock.Addr(), options...) c, err := Dial(mock.Addr(), options...)
if err != nil { require.NoError(t, err)
t.Fatal(err)
}
err = c.Login("anonymous", "anonymous") err = c.Login("anonymous", "anonymous")
if err != nil { require.NoError(t, err)
t.Fatal(err)
}
return mock, c return mock, c
} }
@@ -417,9 +430,7 @@ func closeConn(t *testing.T, mock *ftpMock, c *ServerConn, commands []string) {
// Wait for the connection to close // Wait for the connection to close
mock.Wait() mock.Wait()
if !reflect.DeepEqual(mock.commands, expected) { assert.Equal(t, expected, mock.commands, "unexpected sequence of commands")
t.Fatal("unexpected sequence of commands:", mock.commands, "expected:", expected)
}
} }
func TestConn4(t *testing.T) { func TestConn4(t *testing.T) {

183
ftp.go
View File

@@ -18,6 +18,12 @@ import (
"github.com/hashicorp/go-multierror" "github.com/hashicorp/go-multierror"
) )
const (
// 30 seconds was chosen as it's the
// same duration as http.DefaultTransport's timeout.
DefaultDialTimeout = 30 * time.Second
)
// EntryType describes the different types of an Entry. // EntryType describes the different types of an Entry.
type EntryType int type EntryType int
@@ -28,6 +34,15 @@ const (
EntryTypeLink EntryTypeLink
) )
// TransferType denotes the formats for transferring Entries.
type TransferType string
// The different transfer types
const (
TransferTypeBinary = TransferType("I")
TransferTypeASCII = TransferType("A")
)
// Time format used by the MDTM and MFMT commands // Time format used by the MDTM and MFMT commands
const timeFormat = "20060102150405" const timeFormat = "20060102150405"
@@ -61,11 +76,11 @@ type dialOptions struct {
dialer net.Dialer dialer net.Dialer
tlsConfig *tls.Config tlsConfig *tls.Config
explicitTLS bool explicitTLS bool
conn net.Conn
disableEPSV bool disableEPSV bool
disableUTF8 bool disableUTF8 bool
disableMLSD bool disableMLSD bool
writingMDTM bool writingMDTM bool
forceListHidden bool
location *time.Location location *time.Location
debugOutput io.Writer debugOutput io.Writer
dialFunc func(network, address string) (net.Conn, error) dialFunc func(network, address string) (net.Conn, error)
@@ -99,28 +114,40 @@ func Dial(addr string, options ...DialOption) (*ServerConn, error) {
do.location = time.UTC do.location = time.UTC
} }
tconn := do.conn dialFunc := do.dialFunc
if tconn == nil {
var err error
if do.dialFunc != nil { if dialFunc == nil {
tconn, err = do.dialFunc("tcp", addr)
} else if do.tlsConfig != nil && !do.explicitTLS {
tconn, err = tls.DialWithDialer(&do.dialer, "tcp", addr, do.tlsConfig)
} else {
ctx := do.context ctx := do.context
if ctx == nil { if ctx == nil {
ctx = context.Background() ctx = context.Background()
} }
if _, ok := ctx.Deadline(); !ok {
tconn, err = do.dialer.DialContext(ctx, "tcp", addr) var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, DefaultDialTimeout)
defer cancel()
} }
if do.tlsConfig != nil && !do.explicitTLS {
dialFunc = func(network, address string) (net.Conn, error) {
tlsDialer := &tls.Dialer{
NetDialer: &do.dialer,
Config: do.tlsConfig,
}
return tlsDialer.DialContext(ctx, network, addr)
}
} else {
dialFunc = func(network, address string) (net.Conn, error) {
return do.dialer.DialContext(ctx, network, addr)
}
}
}
tconn, err := dialFunc("tcp", addr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
}
// Use the resolved IP address in case addr contains a domain name // Use the resolved IP address in case addr contains a domain name
// If we use the domain name, we might not resolve to the same IP. // If we use the domain name, we might not resolve to the same IP.
@@ -134,7 +161,7 @@ func Dial(addr string, options ...DialOption) (*ServerConn, error) {
host: remoteAddr.IP.String(), host: remoteAddr.IP.String(),
} }
_, _, err := c.conn.ReadResponse(StatusReady) _, _, err = c.conn.ReadResponse(StatusReady)
if err != nil { if err != nil {
_ = c.Quit() _ = c.Quit()
return nil, err return nil, err
@@ -176,10 +203,12 @@ func DialWithDialer(dialer net.Dialer) DialOption {
} }
// DialWithNetConn returns a DialOption that configures the ServerConn with the underlying net.Conn // DialWithNetConn returns a DialOption that configures the ServerConn with the underlying net.Conn
//
// Deprecated: Use [DialWithDialFunc] instead
func DialWithNetConn(conn net.Conn) DialOption { func DialWithNetConn(conn net.Conn) DialOption {
return DialOption{func(do *dialOptions) { return DialWithDialFunc(func(network, address string) (net.Conn, error) {
do.conn = conn return conn, nil
}} })
} }
// DialWithDisabledEPSV returns a DialOption that configures the ServerConn with EPSV disabled // DialWithDisabledEPSV returns a DialOption that configures the ServerConn with EPSV disabled
@@ -219,6 +248,16 @@ func DialWithWritingMDTM(enabled bool) DialOption {
}} }}
} }
// DialWithForceListHidden returns a DialOption making ServerConn use LIST -a to include hidden files and folders in directory listings
//
// This is useful for servers that do not do this by default, but it forces the use of the LIST command
// even if the server supports MLST.
func DialWithForceListHidden(enabled bool) DialOption {
return DialOption{func(do *dialOptions) {
do.forceListHidden = enabled
}}
}
// DialWithLocation returns a DialOption that configures the ServerConn with specified time.Location // DialWithLocation returns a DialOption that configures the ServerConn with specified time.Location
// The location is used to parse the dates sent by the server which are in server's timezone // The location is used to parse the dates sent by the server which are in server's timezone
func DialWithLocation(location *time.Location) DialOption { func DialWithLocation(location *time.Location) DialOption {
@@ -292,14 +331,15 @@ func (o *dialOptions) wrapStream(rd io.ReadCloser) io.ReadCloser {
} }
// Connect is an alias to Dial, for backward compatibility // Connect is an alias to Dial, for backward compatibility
//
// Deprecated: Use [Dial] instead
func Connect(addr string) (*ServerConn, error) { func Connect(addr string) (*ServerConn, error) {
return Dial(addr) return Dial(addr)
} }
// DialTimeout initializes the connection to the specified ftp server address. // DialTimeout initializes the connection to the specified ftp server address.
// //
// It is generally followed by a call to Login() as most FTP commands require // Deprecated: Use [Dial] with [DialWithTimeout] option instead
// an authenticated user.
func DialTimeout(addr string, timeout time.Duration) (*ServerConn, error) { func DialTimeout(addr string, timeout time.Duration) (*ServerConn, error) {
return Dial(addr, DialWithTimeout(timeout)) return Dial(addr, DialWithTimeout(timeout))
} }
@@ -340,7 +380,7 @@ func (c *ServerConn) Login(user, password string) error {
c.mdtmCanWrite = c.mdtmSupported && c.options.writingMDTM c.mdtmCanWrite = c.mdtmSupported && c.options.writingMDTM
// Switch to binary mode // Switch to binary mode
if _, _, err = c.cmd(StatusCommandOK, "TYPE I"); err != nil { if err = c.Type(TransferTypeBinary); err != nil {
return err return err
} }
@@ -519,7 +559,24 @@ func (c *ServerConn) openDataConn() (net.Conn, error) {
} }
if c.options.tlsConfig != nil { if c.options.tlsConfig != nil {
return tls.DialWithDialer(&c.options.dialer, "tcp", addr, c.options.tlsConfig) // We don't use tls.DialWithDialer here (which does Dial, create
// the Client and then do the Handshake) because it seems to
// hang with some FTP servers, namely proftpd and pureftpd.
//
// Instead we do Dial, create the Client and wait for the first
// Read or Write to trigger the Handshake.
//
// This means that if we are uploading a zero sized file, we
// need to make sure we do the Handshake explicitly as Write
// won't have been called. This is done in StorFrom().
//
// See: https://github.com/jlaffaye/ftp/issues/282
conn, err := c.options.dialer.Dial("tcp", addr)
if err != nil {
return nil, err
}
tlsConn := tls.Client(conn, c.options.tlsConfig)
return tlsConn, nil
} }
return c.options.dialer.Dial("tcp", addr) return c.options.dialer.Dial("tcp", addr)
@@ -580,6 +637,12 @@ func (c *ServerConn) cmdDataConnFrom(offset uint64, format string, args ...inter
return conn, nil return conn, nil
} }
// Type switches the transfer mode for the connection.
func (c *ServerConn) Type(transferType TransferType) (err error) {
_, _, err = c.cmd(StatusCommandOK, "TYPE "+string(transferType))
return err
}
// NameList issues an NLST FTP command. // NameList issues an NLST FTP command.
func (c *ServerConn) NameList(path string) (entries []string, err error) { func (c *ServerConn) NameList(path string) (entries []string, err error) {
space := " " space := " "
@@ -615,11 +678,14 @@ func (c *ServerConn) List(path string) (entries []*Entry, err error) {
var cmd string var cmd string
var parser parseFunc var parser parseFunc
if c.mlstSupported { if c.mlstSupported && !c.options.forceListHidden {
cmd = "MLSD" cmd = "MLSD"
parser = parseRFC3659ListLine parser = parseRFC3659ListLine
} else { } else {
cmd = "LIST" cmd = "LIST"
if c.options.forceListHidden {
cmd += " -a"
}
parser = parseListLine parser = parseListLine
} }
@@ -640,9 +706,7 @@ func (c *ServerConn) List(path string) (entries []*Entry, err error) {
now := time.Now() now := time.Now()
for scanner.Scan() { for scanner.Scan() {
entry, errParse := parser(scanner.Text(), now, c.options.location) entry, errParse := parser(scanner.Text(), now, c.options.location)
if errParse != nil { if errParse == nil {
errs = multierror.Append(errs, errParse)
} else {
entries = append(entries, entry) entries = append(entries, entry)
} }
} }
@@ -657,6 +721,62 @@ func (c *ServerConn) List(path string) (entries []*Entry, err error) {
return entries, errs.ErrorOrNil() return entries, errs.ErrorOrNil()
} }
// GetEntry issues a MLST FTP command which retrieves one single Entry using the
// control connection. The returnedEntry will describe the current directory
// when no path is given.
func (c *ServerConn) GetEntry(path string) (entry *Entry, err error) {
if !c.mlstSupported {
return nil, &textproto.Error{Code: StatusNotImplemented, Msg: StatusText(StatusNotImplemented)}
}
space := " "
if path == "" {
space = ""
}
_, msg, err := c.cmd(StatusRequestedFileActionOK, "%s%s%s", "MLST", space, path)
if err != nil {
return nil, err
}
// The expected reply will look something like:
//
// 250-File details
// Type=file;Size=1024;Modify=20220813133357; path
// 250 End
//
// Multiple lines are allowed though, so it can also be in the form:
//
// 250-File details
// Type=file;Size=1024; path
// Modify=20220813133357; path
// 250 End
lines := strings.Split(msg, "\n")
lc := len(lines)
// lines must be a multi-line message with a length of 3 or more, and we
// don't care about the first and last line
if lc < 3 {
return nil, errors.New("invalid response")
}
e := &Entry{}
for _, l := range lines[1 : lc-1] {
// According to RFC 3659, the entry lines must start with a space when passed over the
// control connection. Some servers don't seem to add that space though. Both forms are
// accepted here.
if len(l) > 0 && l[0] == ' ' {
l = l[1:]
}
// Some severs seem to send a blank line at the end which we ignore
if l == "" {
continue
}
if e, err = parseNextRFC3659ListLine(l, c.options.location, e); err != nil {
return nil, err
}
}
return e, nil
}
// IsTimePreciseInList returns true if client and server support the MLSD // IsTimePreciseInList returns true if client and server support the MLSD
// command so List can return time with 1-second precision for all files. // command so List can return time with 1-second precision for all files.
func (c *ServerConn) IsTimePreciseInList() bool { func (c *ServerConn) IsTimePreciseInList() bool {
@@ -813,8 +933,21 @@ func (c *ServerConn) StorFrom(path string, r io.Reader, offset uint64) error {
// response otherwise if the failure is not due to a connection problem, // response otherwise if the failure is not due to a connection problem,
// for example the server denied the upload for quota limits, we miss // for example the server denied the upload for quota limits, we miss
// the response and we cannot use the connection to send other commands. // the response and we cannot use the connection to send other commands.
if _, err := io.Copy(conn, r); err != nil { if n, err := io.Copy(conn, r); err != nil {
errs = multierror.Append(errs, err) errs = multierror.Append(errs, err)
} else if n == 0 {
// If we wrote no bytes and got no error, make sure we call
// tls.Handshake on the connection as it won't get called
// unless Write() is called. (See comment in openDataConn()).
//
// ProFTP doesn't like this and returns "Unable to build data
// connection: Operation not permitted" when trying to upload
// an empty file without this.
if do, ok := conn.(interface{ Handshake() error }); ok {
if err := do.Handshake(); err != nil {
errs = multierror.Append(errs, err)
}
}
} }
if err := conn.Close(); err != nil { if err := conn.Close(); err != nil {

6
go.mod
View File

@@ -4,12 +4,12 @@ go 1.17
require ( require (
github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-multierror v1.1.1
github.com/stretchr/testify v1.6.1 github.com/stretchr/testify v1.8.3
) )
require ( require (
github.com/davecgh/go-spew v1.1.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

14
go.sum
View File

@@ -1,5 +1,6 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
@@ -7,9 +8,14 @@ github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -27,7 +27,11 @@ var dirTimeFormats = []string{
} }
// parseRFC3659ListLine parses the style of directory line defined in RFC 3659. // parseRFC3659ListLine parses the style of directory line defined in RFC 3659.
func parseRFC3659ListLine(line string, now time.Time, loc *time.Location) (*Entry, error) { func parseRFC3659ListLine(line string, _ time.Time, loc *time.Location) (*Entry, error) {
return parseNextRFC3659ListLine(line, loc, &Entry{})
}
func parseNextRFC3659ListLine(line string, loc *time.Location, e *Entry) (*Entry, error) {
iSemicolon := strings.Index(line, ";") iSemicolon := strings.Index(line, ";")
iWhitespace := strings.Index(line, " ") iWhitespace := strings.Index(line, " ")
@@ -35,8 +39,12 @@ func parseRFC3659ListLine(line string, now time.Time, loc *time.Location) (*Entr
return nil, errUnsupportedListLine return nil, errUnsupportedListLine
} }
e := &Entry{ name := line[iWhitespace+1:]
Name: line[iWhitespace+1:], if e.Name == "" {
e.Name = name
} else if e.Name != name {
// All lines must have the same name
return nil, errUnsupportedListLine
} }
for _, field := range strings.Split(line[:iWhitespace-1], ";") { for _, field := range strings.Split(line[:iWhitespace-1], ";") {