ARROW-17045: [C++] Reject trailing slashes on file path - #13577

Merged
pitrou merged 7 commits into
apache:masterfrom
wjones127:ARROW-17045-gcs-slashes
Jul 13, 2022
Merged

ARROW-17045: [C++] Reject trailing slashes on file path#13577
pitrou merged 7 commits into
apache:masterfrom
wjones127:ARROW-17045-gcs-slashes

Conversation

@wjones127

@wjones127wjones127 commented Jul 11, 2022

Copy link
Copy Markdown
Member

BREAKING CHANGE: We had several different behaviors when passing in file paths with trailing slashes: LocalFileSystem would return IOError, S3 would trim off the trailing slash, and GCS would keep the trailing slash as part of the file name (later creating confusion as the file would be labelled a "directory" in list calls). This PR moves them all to the behavior of LocalFileSystem: return IOError.

The R filesystem bindings relied on the behavior provided by S3, since FileSystem$path() returns a SubTreeFileSystem as a convenient way to bundle a path and filesystem object and SubTreeFileSystem$base_path adds a trailing slash. To adapt to the C++ changes, the functions accepting SubTreeFileSystem as a path to a file now modified to trim the trailing slash before passing down to C++.

Here is an example of the differences in behavior between S3 and GCS:

importpyarrow.fsfrompyarrow.fsimportFileSelectorfromdatetimeimporttimedeltagcs=pyarrow.fs.GcsFileSystem(
endpoint_override="localhost:9001",
scheme="http",
anonymous=True,
retry_time_limit=timedelta(seconds=1),
)
gcs.create_dir("py_test")
# Writing to test.txt with and without slash produces a file and a directory!?withgcs.open_output_stream("py_test/test.txt") asout_stream:
out_stream.write(b"Hello world!")
withgcs.open_output_stream("py_test/test.txt/") asout_stream:
out_stream.write(b"Hello world!")
gcs.get_file_info(FileSelector("py_test"))
# [<FileInfo for 'py_test/test.txt': type=FileType.File, size=12>, <FileInfo for 'py_test/test.txt': type=FileType.Directory>]s3=pyarrow.fs.S3FileSystem(
access_key="minioadmin",
secret_key="minioadmin",
scheme="http",
endpoint_override="localhost:9000",
allow_bucket_creation=True,
allow_bucket_deletion=True,
)
s3.create_dir("py-test")
# Writing to test.txt with and without slash writes to same filewiths3.open_output_stream("py-test/test.txt") asout_stream:
out_stream.write(b"Hello world!")
withs3.open_output_stream("py-test/test.txt/") asout_stream:
out_stream.write(b"Hello world!")
s3.get_file_info(FileSelector("py-test"))
# [<FileInfo for 'py-test/test.txt': type=FileType.File, size=12>]

@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

⚠️ Ticket has not been started in JIRA, please click 'Start Progress'.

@wjones127

Copy link
Copy Markdown
MemberAuthor

cc @emkornfield@pitrou@coryan Does this behavior seem reasonable to you?

@emkornfield

Copy link
Copy Markdown
Contributor

Seems reasonable to me. Is it possible to test this against a generated dataset to make sure nothing breaks there?

@wjones127

Copy link
Copy Markdown
MemberAuthor

Seems reasonable to me. Is it possible to test this against a generated dataset to make sure nothing breaks there?

Do these tests from R seem sufficient? https://github.com/apache/arrow/blob/81af2912b5e088c83631df4cfd52e6d6df2878dc/r/tests/testthat/helper-filesystems.R#L130-L156

I pulled the changes from this PR into #13542, so the above tests will run in CI there. (I can confirm they pass locally for GCS and S3.)

@wjones127
wjones127 marked this pull request as ready for review July 12, 2022 04:55
@jorisvandenbossche

Copy link
Copy Markdown
Member

Just wondering: to what extent is this a corner case that just happens to be in our tests, or is there a practical value in allowing trailing slashes?

Because for example also the LocalFileSystem does not allow trailing slashes in a similar example:

In [51]: from pyarrow.fs import LocalFileSystem
In [52]: local = LocalFileSystem()
In [53]: local.create_dir("py-test")
In [54]: with local.open_output_stream("py-test/test.txt/") as out_stream:
...: out_stream.write(b"Hello world!")
...
IsADirectoryError: [Errno 21] Failed to open local file 'py-test/test.txt/'
Detail: [errno 21] Is a directory

(which is an error that makes sense to me, also Python's open will complain that it is a directory and not a file in the equivalent example)

@pitrou

Copy link
Copy Markdown
Member

I agree it seems better to disallow trailing slashes in filenames rather than silently dropping them.

@wjones127

Copy link
Copy Markdown
MemberAuthor

Just wondering: to what extent is this a corner case that just happens to be in our tests, or is there a practical value in allowing trailing slashes?

I'm not sure yet what else relies on that. In R, it's just that we pass fs$path("some/path") to functions like write_parquet(). fs$path() returns a SubTreeFilesystem, which I think is just used as a convenient way to pass a path (as the base path) and a filesystem in one object. But as a side effect of using base_path to transmit the path, we add a trailing slash. Using this method fails for LocalFileSystem though:

library(arrow)
#> #> Attaching package: 'arrow'#> The following object is masked from 'package:utils':#> #> timestampexample_data<- arrow_table(x=Array$create(c(1, 2, 3)))
fs<-LocalFileSystem$create()
write_parquet(example_data, fs$path("test.parquet"))
#> Error: IOError: Failed to open local file 'test.parquet/'#> /Users/willjones/Documents/arrows/arrow/cpp/src/arrow/filesystem/localfs.cc:442 ::arrow::internal::FileOpenWritable(fn, write_only, truncate, append). Detail: [errno 2] No such file or directory

Created on 2022-07-12 by the reprex package (v2.0.1)

cc @nealrichardson in case he has any input.

I will see about changing GCS to reject file paths that end with slashes. Should I leave S3 alone? Or should it also reject them?

@pitrou

Copy link
Copy Markdown
Member

Hmm, SubTreeFilesystem is meant to take a subdirectory parameter, you are not expected to use it to pass a filename.

@pitrou

Copy link
Copy Markdown
Member

Should I leave S3 alone? Or should it also reject them?

It would be fine with me to remove them, but if you hit too many regressions then no need to sweat over it either :-)

@nealrichardson

Copy link
Copy Markdown
Member

Hmm, SubTreeFilesystem is meant to take a subdirectory parameter, you are not expected to use it to pass a filename.

Historical context: ARROW-10254, which points to #8351 (comment)

We could have the single-file writers (perhaps the readers too) in R prune a possible trailing slash from filenames, if this is the only source of the issue. That logic is pretty well encapsulated in make_readable_file() and make_output_stream() so it should be feasible to do. Shouldn't be a concern for write_dataset/open_dataset since they point at directories.

Comment on lines +143 to +145
Status AssertNoTrailingSlash(const std::string& key) {
if (key.back() == '/') {
return NotAFile(key);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would take util::string_view instead but NotAFile only accepts const std::string&. Would it be alright if I moved "arrow/filesystem/util_internal.h" to use util::string_view?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely ok!

@wjones127wjones127 changed the title ARROW-17045: [C++] Ignore trailing slashes on files in GCSARROW-17045: [C++] Reject trailing slashes on file pathJul 12, 2022
Comment threadr/R/io.R Outdated
Comment threadr/R/io.R
file <- file$base_path
# SubTreeFileSystem adds a slash to base_path, but filesystems will reject file names
# with trailing slashes, so we need to remove it here.
file <- sub("/$", "", file$base_path)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm probably misreading this, but is this treating any SubTreeFileSystem as pointing to a local file path?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By "filesystems" I meant the Arrow FileSystem classes, not the local file system. Does that clarify your confusion? Or are you asking something else?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I am asking something else. It seems that this code is replacing file (a FileSystem instance) with a file path, is that right? And below, the file path file will be treated as a local filesystem path?

@wjones127wjones127Jul 13, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see. Yeah I just blindly updated this without thinking about what happens downstream 🤦

Below it will go through the code path where is.string(file) and !is.null(filesystem) are both TRUE, so it will later call file <- filesystem$OpenInputFile(file). So it went from SubTreeFilesystem to a CharacterVector to a <whatever OpenInputFile returns>. Wow that is some very dynamic typing 😵

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the path should be treated as correct filesystem since we extracted that out in the line before.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see, I had missed the filesystem <- file$base_fs. So I guess my last question is: why are we calling make_readable_file with a SubTreeFileSystem as the first argument?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expectation is that users will call FileSystem$path() to specify the location to write/read to:

fs<-S3FileSystem$create()
write_parquet(my_tab, fs$path("my/path/to"))

fs$path returns a SubTreeFileSystem as a convenient way to bundle a path and filesystem object in a single object. See discussion: #8351 (comment)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah. That's unfortunate, but that's not this PR's business. Thanks for the details!

@nealrichardsonnealrichardson left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R side LGTM, thanks!

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 from me, this is a reasonable straightening of the API. Thank you @wjones127 for doing this!

@pitrou
pitrou merged commit 0024962 into apache:masterJul 13, 2022
@wjones127
wjones127 deleted the ARROW-17045-gcs-slashes branch July 13, 2022 17:12
@jorisvandenbossche

Copy link
Copy Markdown
Member

There are some HDFS failures that might be related to this change? See eg https://github.com/ursacomputing/crossbow/runs/7331664487?check_suite_focus=true

@wjones127

Copy link
Copy Markdown
MemberAuthor

Oh no! Those do look related @jorisvandenbossche. Is HDFS not in our usual set of CI tests?

@ursabot

Copy link
Copy Markdown

Benchmark runs are scheduled for baseline = 03e80dc and contender = 0024962. 0024962 is a master commit associated with this PR. Results will be available as each benchmark for each run completes.
Conbench compare runs links:
[Finished ⬇️0.0% ⬆️0.0%] ec2-t3-xlarge-us-east-2
[Finished ⬇️0.75% ⬆️0.1%] test-mac-arm
[Failed ⬇️0.57% ⬆️0.0%] ursa-i9-9960x
[Finished ⬇️2.32% ⬆️0.11%] ursa-thinkcentre-m75q
Buildkite builds:
[Finished] 0024962f ec2-t3-xlarge-us-east-2
[Finished] 0024962f test-mac-arm
[Failed] 0024962f ursa-i9-9960x
[Finished] 0024962f ursa-thinkcentre-m75q
[Finished] 03e80dc1 ec2-t3-xlarge-us-east-2
[Finished] 03e80dc1 test-mac-arm
[Failed] 03e80dc1 ursa-i9-9960x
[Finished] 03e80dc1 ursa-thinkcentre-m75q
Supported benchmarks:
ec2-t3-xlarge-us-east-2: Supported benchmark langs: Python, R. Runs only benchmarks with cloud = True
test-mac-arm: Supported benchmark langs: C++, Python, R
ursa-i9-9960x: Supported benchmark langs: Python, R, JavaScript
ursa-thinkcentre-m75q: Supported benchmark langs: C++, Java

@pitrou

Copy link
Copy Markdown
Member

@wjones127 They are in the nightly builds but not in the PR checks. Look for hdfs in archery docker images and you'll probably be able to reproduce locally :-)

pitrou pushed a commit that referenced this pull request Jul 19, 2022
…13615)
Follow up to #13577 / ARROW-17045.
Authored-by: Will Jones <willjones127@gmail.com>
Signed-off-by: Antoine Pitrou <antoine@python.org>
Tom-Newton added a commit to Tom-Newton/arrow that referenced this pull request Oct 16, 2023
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@wjones127@emkornfield@jorisvandenbossche@pitrou@nealrichardson@ursabot
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

ARROW-17045: [C++] Reject trailing slashes on file path - #13577

Merged
pitrou merged 7 commits into
apache:masterfrom
wjones127:ARROW-17045-gcs-slashes
Jul 13, 2022
Merged

ARROW-17045: [C++] Reject trailing slashes on file path#13577
pitrou merged 7 commits into
apache:masterfrom
wjones127:ARROW-17045-gcs-slashes

Conversation

@wjones127

@wjones127wjones127 commented Jul 11, 2022

Copy link
Copy Markdown
Member

BREAKING CHANGE: We had several different behaviors when passing in file paths with trailing slashes: LocalFileSystem would return IOError, S3 would trim off the trailing slash, and GCS would keep the trailing slash as part of the file name (later creating confusion as the file would be labelled a "directory" in list calls). This PR moves them all to the behavior of LocalFileSystem: return IOError.

The R filesystem bindings relied on the behavior provided by S3, since FileSystem$path() returns a SubTreeFileSystem as a convenient way to bundle a path and filesystem object and SubTreeFileSystem$base_path adds a trailing slash. To adapt to the C++ changes, the functions accepting SubTreeFileSystem as a path to a file now modified to trim the trailing slash before passing down to C++.

Here is an example of the differences in behavior between S3 and GCS:

importpyarrow.fsfrompyarrow.fsimportFileSelectorfromdatetimeimporttimedeltagcs=pyarrow.fs.GcsFileSystem(
endpoint_override="localhost:9001",
scheme="http",
anonymous=True,
retry_time_limit=timedelta(seconds=1),
)
gcs.create_dir("py_test")
# Writing to test.txt with and without slash produces a file and a directory!?withgcs.open_output_stream("py_test/test.txt") asout_stream:
out_stream.write(b"Hello world!")
withgcs.open_output_stream("py_test/test.txt/") asout_stream:
out_stream.write(b"Hello world!")
gcs.get_file_info(FileSelector("py_test"))
# [<FileInfo for 'py_test/test.txt': type=FileType.File, size=12>, <FileInfo for 'py_test/test.txt': type=FileType.Directory>]s3=pyarrow.fs.S3FileSystem(
access_key="minioadmin",
secret_key="minioadmin",
scheme="http",
endpoint_override="localhost:9000",
allow_bucket_creation=True,
allow_bucket_deletion=True,
)
s3.create_dir("py-test")
# Writing to test.txt with and without slash writes to same filewiths3.open_output_stream("py-test/test.txt") asout_stream:
out_stream.write(b"Hello world!")
withs3.open_output_stream("py-test/test.txt/") asout_stream:
out_stream.write(b"Hello world!")
s3.get_file_info(FileSelector("py-test"))
# [<FileInfo for 'py-test/test.txt': type=FileType.File, size=12>]

@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

⚠️ Ticket has not been started in JIRA, please click 'Start Progress'.

@wjones127

Copy link
Copy Markdown
MemberAuthor

cc @emkornfield@pitrou@coryan Does this behavior seem reasonable to you?

@emkornfield

Copy link
Copy Markdown
Contributor

Seems reasonable to me. Is it possible to test this against a generated dataset to make sure nothing breaks there?

@wjones127

Copy link
Copy Markdown
MemberAuthor

Seems reasonable to me. Is it possible to test this against a generated dataset to make sure nothing breaks there?

Do these tests from R seem sufficient? https://github.com/apache/arrow/blob/81af2912b5e088c83631df4cfd52e6d6df2878dc/r/tests/testthat/helper-filesystems.R#L130-L156

I pulled the changes from this PR into #13542, so the above tests will run in CI there. (I can confirm they pass locally for GCS and S3.)

@wjones127
wjones127 marked this pull request as ready for review July 12, 2022 04:55
@jorisvandenbossche

Copy link
Copy Markdown
Member

Just wondering: to what extent is this a corner case that just happens to be in our tests, or is there a practical value in allowing trailing slashes?

Because for example also the LocalFileSystem does not allow trailing slashes in a similar example:

In [51]: from pyarrow.fs import LocalFileSystem
In [52]: local = LocalFileSystem()
In [53]: local.create_dir("py-test")
In [54]: with local.open_output_stream("py-test/test.txt/") as out_stream:
...: out_stream.write(b"Hello world!")
...
IsADirectoryError: [Errno 21] Failed to open local file 'py-test/test.txt/'
Detail: [errno 21] Is a directory

(which is an error that makes sense to me, also Python's open will complain that it is a directory and not a file in the equivalent example)

@pitrou

Copy link
Copy Markdown
Member

I agree it seems better to disallow trailing slashes in filenames rather than silently dropping them.

@wjones127

Copy link
Copy Markdown
MemberAuthor

Just wondering: to what extent is this a corner case that just happens to be in our tests, or is there a practical value in allowing trailing slashes?

I'm not sure yet what else relies on that. In R, it's just that we pass fs$path("some/path") to functions like write_parquet(). fs$path() returns a SubTreeFilesystem, which I think is just used as a convenient way to pass a path (as the base path) and a filesystem in one object. But as a side effect of using base_path to transmit the path, we add a trailing slash. Using this method fails for LocalFileSystem though:

library(arrow)
#> #> Attaching package: 'arrow'#> The following object is masked from 'package:utils':#> #> timestampexample_data<- arrow_table(x=Array$create(c(1, 2, 3)))
fs<-LocalFileSystem$create()
write_parquet(example_data, fs$path("test.parquet"))
#> Error: IOError: Failed to open local file 'test.parquet/'#> /Users/willjones/Documents/arrows/arrow/cpp/src/arrow/filesystem/localfs.cc:442 ::arrow::internal::FileOpenWritable(fn, write_only, truncate, append). Detail: [errno 2] No such file or directory

Created on 2022-07-12 by the reprex package (v2.0.1)

cc @nealrichardson in case he has any input.

I will see about changing GCS to reject file paths that end with slashes. Should I leave S3 alone? Or should it also reject them?

@pitrou

Copy link
Copy Markdown
Member

Hmm, SubTreeFilesystem is meant to take a subdirectory parameter, you are not expected to use it to pass a filename.

@pitrou

Copy link
Copy Markdown
Member

Should I leave S3 alone? Or should it also reject them?

It would be fine with me to remove them, but if you hit too many regressions then no need to sweat over it either :-)

@nealrichardson

Copy link
Copy Markdown
Member

Hmm, SubTreeFilesystem is meant to take a subdirectory parameter, you are not expected to use it to pass a filename.

Historical context: ARROW-10254, which points to #8351 (comment)

We could have the single-file writers (perhaps the readers too) in R prune a possible trailing slash from filenames, if this is the only source of the issue. That logic is pretty well encapsulated in make_readable_file() and make_output_stream() so it should be feasible to do. Shouldn't be a concern for write_dataset/open_dataset since they point at directories.

Comment on lines +143 to +145
Status AssertNoTrailingSlash(const std::string& key) {
if (key.back() == '/') {
return NotAFile(key);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would take util::string_view instead but NotAFile only accepts const std::string&. Would it be alright if I moved "arrow/filesystem/util_internal.h" to use util::string_view?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely ok!

@wjones127wjones127 changed the title ARROW-17045: [C++] Ignore trailing slashes on files in GCSARROW-17045: [C++] Reject trailing slashes on file pathJul 12, 2022
Comment threadr/R/io.R Outdated
Comment threadr/R/io.R
file <- file$base_path
# SubTreeFileSystem adds a slash to base_path, but filesystems will reject file names
# with trailing slashes, so we need to remove it here.
file <- sub("/$", "", file$base_path)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm probably misreading this, but is this treating any SubTreeFileSystem as pointing to a local file path?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By "filesystems" I meant the Arrow FileSystem classes, not the local file system. Does that clarify your confusion? Or are you asking something else?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I am asking something else. It seems that this code is replacing file (a FileSystem instance) with a file path, is that right? And below, the file path file will be treated as a local filesystem path?

@wjones127wjones127Jul 13, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see. Yeah I just blindly updated this without thinking about what happens downstream 🤦

Below it will go through the code path where is.string(file) and !is.null(filesystem) are both TRUE, so it will later call file <- filesystem$OpenInputFile(file). So it went from SubTreeFilesystem to a CharacterVector to a <whatever OpenInputFile returns>. Wow that is some very dynamic typing 😵

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the path should be treated as correct filesystem since we extracted that out in the line before.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see, I had missed the filesystem <- file$base_fs. So I guess my last question is: why are we calling make_readable_file with a SubTreeFileSystem as the first argument?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expectation is that users will call FileSystem$path() to specify the location to write/read to:

fs<-S3FileSystem$create()
write_parquet(my_tab, fs$path("my/path/to"))

fs$path returns a SubTreeFileSystem as a convenient way to bundle a path and filesystem object in a single object. See discussion: #8351 (comment)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah. That's unfortunate, but that's not this PR's business. Thanks for the details!

@nealrichardsonnealrichardson left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R side LGTM, thanks!

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 from me, this is a reasonable straightening of the API. Thank you @wjones127 for doing this!

@pitrou
pitrou merged commit 0024962 into apache:masterJul 13, 2022
@wjones127
wjones127 deleted the ARROW-17045-gcs-slashes branch July 13, 2022 17:12
@jorisvandenbossche

Copy link
Copy Markdown
Member

There are some HDFS failures that might be related to this change? See eg https://github.com/ursacomputing/crossbow/runs/7331664487?check_suite_focus=true

@wjones127

Copy link
Copy Markdown
MemberAuthor

Oh no! Those do look related @jorisvandenbossche. Is HDFS not in our usual set of CI tests?

@ursabot

Copy link
Copy Markdown

Benchmark runs are scheduled for baseline = 03e80dc and contender = 0024962. 0024962 is a master commit associated with this PR. Results will be available as each benchmark for each run completes.
Conbench compare runs links:
[Finished ⬇️0.0% ⬆️0.0%] ec2-t3-xlarge-us-east-2
[Finished ⬇️0.75% ⬆️0.1%] test-mac-arm
[Failed ⬇️0.57% ⬆️0.0%] ursa-i9-9960x
[Finished ⬇️2.32% ⬆️0.11%] ursa-thinkcentre-m75q
Buildkite builds:
[Finished] 0024962f ec2-t3-xlarge-us-east-2
[Finished] 0024962f test-mac-arm
[Failed] 0024962f ursa-i9-9960x
[Finished] 0024962f ursa-thinkcentre-m75q
[Finished] 03e80dc1 ec2-t3-xlarge-us-east-2
[Finished] 03e80dc1 test-mac-arm
[Failed] 03e80dc1 ursa-i9-9960x
[Finished] 03e80dc1 ursa-thinkcentre-m75q
Supported benchmarks:
ec2-t3-xlarge-us-east-2: Supported benchmark langs: Python, R. Runs only benchmarks with cloud = True
test-mac-arm: Supported benchmark langs: C++, Python, R
ursa-i9-9960x: Supported benchmark langs: Python, R, JavaScript
ursa-thinkcentre-m75q: Supported benchmark langs: C++, Java

@pitrou

Copy link
Copy Markdown
Member

@wjones127 They are in the nightly builds but not in the PR checks. Look for hdfs in archery docker images and you'll probably be able to reproduce locally :-)

pitrou pushed a commit that referenced this pull request Jul 19, 2022
…13615)
Follow up to #13577 / ARROW-17045.
Authored-by: Will Jones <willjones127@gmail.com>
Signed-off-by: Antoine Pitrou <antoine@python.org>
Tom-Newton added a commit to Tom-Newton/arrow that referenced this pull request Oct 16, 2023
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@wjones127@emkornfield@jorisvandenbossche@pitrou@nealrichardson@ursabot
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

ARROW-17045: [C++] Reject trailing slashes on file path - #13577

Merged
pitrou merged 7 commits into
apache:masterfrom
wjones127:ARROW-17045-gcs-slashes
Jul 13, 2022
Merged

ARROW-17045: [C++] Reject trailing slashes on file path#13577
pitrou merged 7 commits into
apache:masterfrom
wjones127:ARROW-17045-gcs-slashes

Conversation

@wjones127

@wjones127wjones127 commented Jul 11, 2022

Copy link
Copy Markdown
Member

BREAKING CHANGE: We had several different behaviors when passing in file paths with trailing slashes: LocalFileSystem would return IOError, S3 would trim off the trailing slash, and GCS would keep the trailing slash as part of the file name (later creating confusion as the file would be labelled a "directory" in list calls). This PR moves them all to the behavior of LocalFileSystem: return IOError.

The R filesystem bindings relied on the behavior provided by S3, since FileSystem$path() returns a SubTreeFileSystem as a convenient way to bundle a path and filesystem object and SubTreeFileSystem$base_path adds a trailing slash. To adapt to the C++ changes, the functions accepting SubTreeFileSystem as a path to a file now modified to trim the trailing slash before passing down to C++.

Here is an example of the differences in behavior between S3 and GCS:

importpyarrow.fsfrompyarrow.fsimportFileSelectorfromdatetimeimporttimedeltagcs=pyarrow.fs.GcsFileSystem(
endpoint_override="localhost:9001",
scheme="http",
anonymous=True,
retry_time_limit=timedelta(seconds=1),
)
gcs.create_dir("py_test")
# Writing to test.txt with and without slash produces a file and a directory!?withgcs.open_output_stream("py_test/test.txt") asout_stream:
out_stream.write(b"Hello world!")
withgcs.open_output_stream("py_test/test.txt/") asout_stream:
out_stream.write(b"Hello world!")
gcs.get_file_info(FileSelector("py_test"))
# [<FileInfo for 'py_test/test.txt': type=FileType.File, size=12>, <FileInfo for 'py_test/test.txt': type=FileType.Directory>]s3=pyarrow.fs.S3FileSystem(
access_key="minioadmin",
secret_key="minioadmin",
scheme="http",
endpoint_override="localhost:9000",
allow_bucket_creation=True,
allow_bucket_deletion=True,
)
s3.create_dir("py-test")
# Writing to test.txt with and without slash writes to same filewiths3.open_output_stream("py-test/test.txt") asout_stream:
out_stream.write(b"Hello world!")
withs3.open_output_stream("py-test/test.txt/") asout_stream:
out_stream.write(b"Hello world!")
s3.get_file_info(FileSelector("py-test"))
# [<FileInfo for 'py-test/test.txt': type=FileType.File, size=12>]

@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

⚠️ Ticket has not been started in JIRA, please click 'Start Progress'.

@wjones127

Copy link
Copy Markdown
MemberAuthor

cc @emkornfield@pitrou@coryan Does this behavior seem reasonable to you?

@emkornfield

Copy link
Copy Markdown
Contributor

Seems reasonable to me. Is it possible to test this against a generated dataset to make sure nothing breaks there?

@wjones127

Copy link
Copy Markdown
MemberAuthor

Seems reasonable to me. Is it possible to test this against a generated dataset to make sure nothing breaks there?

Do these tests from R seem sufficient? https://github.com/apache/arrow/blob/81af2912b5e088c83631df4cfd52e6d6df2878dc/r/tests/testthat/helper-filesystems.R#L130-L156

I pulled the changes from this PR into #13542, so the above tests will run in CI there. (I can confirm they pass locally for GCS and S3.)

@wjones127
wjones127 marked this pull request as ready for review July 12, 2022 04:55
@jorisvandenbossche

Copy link
Copy Markdown
Member

Just wondering: to what extent is this a corner case that just happens to be in our tests, or is there a practical value in allowing trailing slashes?

Because for example also the LocalFileSystem does not allow trailing slashes in a similar example:

In [51]: from pyarrow.fs import LocalFileSystem
In [52]: local = LocalFileSystem()
In [53]: local.create_dir("py-test")
In [54]: with local.open_output_stream("py-test/test.txt/") as out_stream:
...: out_stream.write(b"Hello world!")
...
IsADirectoryError: [Errno 21] Failed to open local file 'py-test/test.txt/'
Detail: [errno 21] Is a directory

(which is an error that makes sense to me, also Python's open will complain that it is a directory and not a file in the equivalent example)

@pitrou

Copy link
Copy Markdown
Member

I agree it seems better to disallow trailing slashes in filenames rather than silently dropping them.

@wjones127

Copy link
Copy Markdown
MemberAuthor

Just wondering: to what extent is this a corner case that just happens to be in our tests, or is there a practical value in allowing trailing slashes?

I'm not sure yet what else relies on that. In R, it's just that we pass fs$path("some/path") to functions like write_parquet(). fs$path() returns a SubTreeFilesystem, which I think is just used as a convenient way to pass a path (as the base path) and a filesystem in one object. But as a side effect of using base_path to transmit the path, we add a trailing slash. Using this method fails for LocalFileSystem though:

library(arrow)
#> #> Attaching package: 'arrow'#> The following object is masked from 'package:utils':#> #> timestampexample_data<- arrow_table(x=Array$create(c(1, 2, 3)))
fs<-LocalFileSystem$create()
write_parquet(example_data, fs$path("test.parquet"))
#> Error: IOError: Failed to open local file 'test.parquet/'#> /Users/willjones/Documents/arrows/arrow/cpp/src/arrow/filesystem/localfs.cc:442 ::arrow::internal::FileOpenWritable(fn, write_only, truncate, append). Detail: [errno 2] No such file or directory

Created on 2022-07-12 by the reprex package (v2.0.1)

cc @nealrichardson in case he has any input.

I will see about changing GCS to reject file paths that end with slashes. Should I leave S3 alone? Or should it also reject them?

@pitrou

Copy link
Copy Markdown
Member

Hmm, SubTreeFilesystem is meant to take a subdirectory parameter, you are not expected to use it to pass a filename.

@pitrou

Copy link
Copy Markdown
Member

Should I leave S3 alone? Or should it also reject them?

It would be fine with me to remove them, but if you hit too many regressions then no need to sweat over it either :-)

@nealrichardson

Copy link
Copy Markdown
Member

Hmm, SubTreeFilesystem is meant to take a subdirectory parameter, you are not expected to use it to pass a filename.

Historical context: ARROW-10254, which points to #8351 (comment)

We could have the single-file writers (perhaps the readers too) in R prune a possible trailing slash from filenames, if this is the only source of the issue. That logic is pretty well encapsulated in make_readable_file() and make_output_stream() so it should be feasible to do. Shouldn't be a concern for write_dataset/open_dataset since they point at directories.

Comment on lines +143 to +145
Status AssertNoTrailingSlash(const std::string& key) {
if (key.back() == '/') {
return NotAFile(key);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would take util::string_view instead but NotAFile only accepts const std::string&. Would it be alright if I moved "arrow/filesystem/util_internal.h" to use util::string_view?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely ok!

@wjones127wjones127 changed the title ARROW-17045: [C++] Ignore trailing slashes on files in GCSARROW-17045: [C++] Reject trailing slashes on file pathJul 12, 2022
Comment threadr/R/io.R Outdated
Comment threadr/R/io.R
file <- file$base_path
# SubTreeFileSystem adds a slash to base_path, but filesystems will reject file names
# with trailing slashes, so we need to remove it here.
file <- sub("/$", "", file$base_path)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm probably misreading this, but is this treating any SubTreeFileSystem as pointing to a local file path?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By "filesystems" I meant the Arrow FileSystem classes, not the local file system. Does that clarify your confusion? Or are you asking something else?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I am asking something else. It seems that this code is replacing file (a FileSystem instance) with a file path, is that right? And below, the file path file will be treated as a local filesystem path?

@wjones127wjones127Jul 13, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see. Yeah I just blindly updated this without thinking about what happens downstream 🤦

Below it will go through the code path where is.string(file) and !is.null(filesystem) are both TRUE, so it will later call file <- filesystem$OpenInputFile(file). So it went from SubTreeFilesystem to a CharacterVector to a <whatever OpenInputFile returns>. Wow that is some very dynamic typing 😵

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the path should be treated as correct filesystem since we extracted that out in the line before.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see, I had missed the filesystem <- file$base_fs. So I guess my last question is: why are we calling make_readable_file with a SubTreeFileSystem as the first argument?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expectation is that users will call FileSystem$path() to specify the location to write/read to:

fs<-S3FileSystem$create()
write_parquet(my_tab, fs$path("my/path/to"))

fs$path returns a SubTreeFileSystem as a convenient way to bundle a path and filesystem object in a single object. See discussion: #8351 (comment)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah. That's unfortunate, but that's not this PR's business. Thanks for the details!

@nealrichardsonnealrichardson left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R side LGTM, thanks!

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 from me, this is a reasonable straightening of the API. Thank you @wjones127 for doing this!

@pitrou
pitrou merged commit 0024962 into apache:masterJul 13, 2022
@wjones127
wjones127 deleted the ARROW-17045-gcs-slashes branch July 13, 2022 17:12
@jorisvandenbossche

Copy link
Copy Markdown
Member

There are some HDFS failures that might be related to this change? See eg https://github.com/ursacomputing/crossbow/runs/7331664487?check_suite_focus=true

@wjones127

Copy link
Copy Markdown
MemberAuthor

Oh no! Those do look related @jorisvandenbossche. Is HDFS not in our usual set of CI tests?

@ursabot

Copy link
Copy Markdown

Benchmark runs are scheduled for baseline = 03e80dc and contender = 0024962. 0024962 is a master commit associated with this PR. Results will be available as each benchmark for each run completes.
Conbench compare runs links:
[Finished ⬇️0.0% ⬆️0.0%] ec2-t3-xlarge-us-east-2
[Finished ⬇️0.75% ⬆️0.1%] test-mac-arm
[Failed ⬇️0.57% ⬆️0.0%] ursa-i9-9960x
[Finished ⬇️2.32% ⬆️0.11%] ursa-thinkcentre-m75q
Buildkite builds:
[Finished] 0024962f ec2-t3-xlarge-us-east-2
[Finished] 0024962f test-mac-arm
[Failed] 0024962f ursa-i9-9960x
[Finished] 0024962f ursa-thinkcentre-m75q
[Finished] 03e80dc1 ec2-t3-xlarge-us-east-2
[Finished] 03e80dc1 test-mac-arm
[Failed] 03e80dc1 ursa-i9-9960x
[Finished] 03e80dc1 ursa-thinkcentre-m75q
Supported benchmarks:
ec2-t3-xlarge-us-east-2: Supported benchmark langs: Python, R. Runs only benchmarks with cloud = True
test-mac-arm: Supported benchmark langs: C++, Python, R
ursa-i9-9960x: Supported benchmark langs: Python, R, JavaScript
ursa-thinkcentre-m75q: Supported benchmark langs: C++, Java

@pitrou

Copy link
Copy Markdown
Member

@wjones127 They are in the nightly builds but not in the PR checks. Look for hdfs in archery docker images and you'll probably be able to reproduce locally :-)

pitrou pushed a commit that referenced this pull request Jul 19, 2022
…13615)
Follow up to #13577 / ARROW-17045.
Authored-by: Will Jones <willjones127@gmail.com>
Signed-off-by: Antoine Pitrou <antoine@python.org>
Tom-Newton added a commit to Tom-Newton/arrow that referenced this pull request Oct 16, 2023
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@wjones127@emkornfield@jorisvandenbossche@pitrou@nealrichardson@ursabot
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

ARROW-17045: [C++] Reject trailing slashes on file path - #13577

Merged
pitrou merged 7 commits into
apache:masterfrom
wjones127:ARROW-17045-gcs-slashes
Jul 13, 2022
Merged

ARROW-17045: [C++] Reject trailing slashes on file path#13577
pitrou merged 7 commits into
apache:masterfrom
wjones127:ARROW-17045-gcs-slashes

Conversation

@wjones127

@wjones127wjones127 commented Jul 11, 2022

Copy link
Copy Markdown
Member

BREAKING CHANGE: We had several different behaviors when passing in file paths with trailing slashes: LocalFileSystem would return IOError, S3 would trim off the trailing slash, and GCS would keep the trailing slash as part of the file name (later creating confusion as the file would be labelled a "directory" in list calls). This PR moves them all to the behavior of LocalFileSystem: return IOError.

The R filesystem bindings relied on the behavior provided by S3, since FileSystem$path() returns a SubTreeFileSystem as a convenient way to bundle a path and filesystem object and SubTreeFileSystem$base_path adds a trailing slash. To adapt to the C++ changes, the functions accepting SubTreeFileSystem as a path to a file now modified to trim the trailing slash before passing down to C++.

Here is an example of the differences in behavior between S3 and GCS:

importpyarrow.fsfrompyarrow.fsimportFileSelectorfromdatetimeimporttimedeltagcs=pyarrow.fs.GcsFileSystem(
endpoint_override="localhost:9001",
scheme="http",
anonymous=True,
retry_time_limit=timedelta(seconds=1),
)
gcs.create_dir("py_test")
# Writing to test.txt with and without slash produces a file and a directory!?withgcs.open_output_stream("py_test/test.txt") asout_stream:
out_stream.write(b"Hello world!")
withgcs.open_output_stream("py_test/test.txt/") asout_stream:
out_stream.write(b"Hello world!")
gcs.get_file_info(FileSelector("py_test"))
# [<FileInfo for 'py_test/test.txt': type=FileType.File, size=12>, <FileInfo for 'py_test/test.txt': type=FileType.Directory>]s3=pyarrow.fs.S3FileSystem(
access_key="minioadmin",
secret_key="minioadmin",
scheme="http",
endpoint_override="localhost:9000",
allow_bucket_creation=True,
allow_bucket_deletion=True,
)
s3.create_dir("py-test")
# Writing to test.txt with and without slash writes to same filewiths3.open_output_stream("py-test/test.txt") asout_stream:
out_stream.write(b"Hello world!")
withs3.open_output_stream("py-test/test.txt/") asout_stream:
out_stream.write(b"Hello world!")
s3.get_file_info(FileSelector("py-test"))
# [<FileInfo for 'py-test/test.txt': type=FileType.File, size=12>]

@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

⚠️ Ticket has not been started in JIRA, please click 'Start Progress'.

@wjones127

Copy link
Copy Markdown
MemberAuthor

cc @emkornfield@pitrou@coryan Does this behavior seem reasonable to you?

@emkornfield

Copy link
Copy Markdown
Contributor

Seems reasonable to me. Is it possible to test this against a generated dataset to make sure nothing breaks there?

@wjones127

Copy link
Copy Markdown
MemberAuthor

Seems reasonable to me. Is it possible to test this against a generated dataset to make sure nothing breaks there?

Do these tests from R seem sufficient? https://github.com/apache/arrow/blob/81af2912b5e088c83631df4cfd52e6d6df2878dc/r/tests/testthat/helper-filesystems.R#L130-L156

I pulled the changes from this PR into #13542, so the above tests will run in CI there. (I can confirm they pass locally for GCS and S3.)

@wjones127
wjones127 marked this pull request as ready for review July 12, 2022 04:55
@jorisvandenbossche

Copy link
Copy Markdown
Member

Just wondering: to what extent is this a corner case that just happens to be in our tests, or is there a practical value in allowing trailing slashes?

Because for example also the LocalFileSystem does not allow trailing slashes in a similar example:

In [51]: from pyarrow.fs import LocalFileSystem
In [52]: local = LocalFileSystem()
In [53]: local.create_dir("py-test")
In [54]: with local.open_output_stream("py-test/test.txt/") as out_stream:
...: out_stream.write(b"Hello world!")
...
IsADirectoryError: [Errno 21] Failed to open local file 'py-test/test.txt/'
Detail: [errno 21] Is a directory

(which is an error that makes sense to me, also Python's open will complain that it is a directory and not a file in the equivalent example)

@pitrou

Copy link
Copy Markdown
Member

I agree it seems better to disallow trailing slashes in filenames rather than silently dropping them.

@wjones127

Copy link
Copy Markdown
MemberAuthor

Just wondering: to what extent is this a corner case that just happens to be in our tests, or is there a practical value in allowing trailing slashes?

I'm not sure yet what else relies on that. In R, it's just that we pass fs$path("some/path") to functions like write_parquet(). fs$path() returns a SubTreeFilesystem, which I think is just used as a convenient way to pass a path (as the base path) and a filesystem in one object. But as a side effect of using base_path to transmit the path, we add a trailing slash. Using this method fails for LocalFileSystem though:

library(arrow)
#> #> Attaching package: 'arrow'#> The following object is masked from 'package:utils':#> #> timestampexample_data<- arrow_table(x=Array$create(c(1, 2, 3)))
fs<-LocalFileSystem$create()
write_parquet(example_data, fs$path("test.parquet"))
#> Error: IOError: Failed to open local file 'test.parquet/'#> /Users/willjones/Documents/arrows/arrow/cpp/src/arrow/filesystem/localfs.cc:442 ::arrow::internal::FileOpenWritable(fn, write_only, truncate, append). Detail: [errno 2] No such file or directory

Created on 2022-07-12 by the reprex package (v2.0.1)

cc @nealrichardson in case he has any input.

I will see about changing GCS to reject file paths that end with slashes. Should I leave S3 alone? Or should it also reject them?

@pitrou

Copy link
Copy Markdown
Member

Hmm, SubTreeFilesystem is meant to take a subdirectory parameter, you are not expected to use it to pass a filename.

@pitrou

Copy link
Copy Markdown
Member

Should I leave S3 alone? Or should it also reject them?

It would be fine with me to remove them, but if you hit too many regressions then no need to sweat over it either :-)

@nealrichardson

Copy link
Copy Markdown
Member

Hmm, SubTreeFilesystem is meant to take a subdirectory parameter, you are not expected to use it to pass a filename.

Historical context: ARROW-10254, which points to #8351 (comment)

We could have the single-file writers (perhaps the readers too) in R prune a possible trailing slash from filenames, if this is the only source of the issue. That logic is pretty well encapsulated in make_readable_file() and make_output_stream() so it should be feasible to do. Shouldn't be a concern for write_dataset/open_dataset since they point at directories.

Comment on lines +143 to +145
Status AssertNoTrailingSlash(const std::string& key) {
if (key.back() == '/') {
return NotAFile(key);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would take util::string_view instead but NotAFile only accepts const std::string&. Would it be alright if I moved "arrow/filesystem/util_internal.h" to use util::string_view?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely ok!

@wjones127wjones127 changed the title ARROW-17045: [C++] Ignore trailing slashes on files in GCSARROW-17045: [C++] Reject trailing slashes on file pathJul 12, 2022
Comment threadr/R/io.R Outdated
Comment threadr/R/io.R
file <- file$base_path
# SubTreeFileSystem adds a slash to base_path, but filesystems will reject file names
# with trailing slashes, so we need to remove it here.
file <- sub("/$", "", file$base_path)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm probably misreading this, but is this treating any SubTreeFileSystem as pointing to a local file path?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By "filesystems" I meant the Arrow FileSystem classes, not the local file system. Does that clarify your confusion? Or are you asking something else?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I am asking something else. It seems that this code is replacing file (a FileSystem instance) with a file path, is that right? And below, the file path file will be treated as a local filesystem path?

@wjones127wjones127Jul 13, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see. Yeah I just blindly updated this without thinking about what happens downstream 🤦

Below it will go through the code path where is.string(file) and !is.null(filesystem) are both TRUE, so it will later call file <- filesystem$OpenInputFile(file). So it went from SubTreeFilesystem to a CharacterVector to a <whatever OpenInputFile returns>. Wow that is some very dynamic typing 😵

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the path should be treated as correct filesystem since we extracted that out in the line before.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see, I had missed the filesystem <- file$base_fs. So I guess my last question is: why are we calling make_readable_file with a SubTreeFileSystem as the first argument?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expectation is that users will call FileSystem$path() to specify the location to write/read to:

fs<-S3FileSystem$create()
write_parquet(my_tab, fs$path("my/path/to"))

fs$path returns a SubTreeFileSystem as a convenient way to bundle a path and filesystem object in a single object. See discussion: #8351 (comment)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah. That's unfortunate, but that's not this PR's business. Thanks for the details!

@nealrichardsonnealrichardson left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R side LGTM, thanks!

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 from me, this is a reasonable straightening of the API. Thank you @wjones127 for doing this!

@pitrou
pitrou merged commit 0024962 into apache:masterJul 13, 2022
@wjones127
wjones127 deleted the ARROW-17045-gcs-slashes branch July 13, 2022 17:12
@jorisvandenbossche

Copy link
Copy Markdown
Member

There are some HDFS failures that might be related to this change? See eg https://github.com/ursacomputing/crossbow/runs/7331664487?check_suite_focus=true

@wjones127

Copy link
Copy Markdown
MemberAuthor

Oh no! Those do look related @jorisvandenbossche. Is HDFS not in our usual set of CI tests?

@ursabot

Copy link
Copy Markdown

Benchmark runs are scheduled for baseline = 03e80dc and contender = 0024962. 0024962 is a master commit associated with this PR. Results will be available as each benchmark for each run completes.
Conbench compare runs links:
[Finished ⬇️0.0% ⬆️0.0%] ec2-t3-xlarge-us-east-2
[Finished ⬇️0.75% ⬆️0.1%] test-mac-arm
[Failed ⬇️0.57% ⬆️0.0%] ursa-i9-9960x
[Finished ⬇️2.32% ⬆️0.11%] ursa-thinkcentre-m75q
Buildkite builds:
[Finished] 0024962f ec2-t3-xlarge-us-east-2
[Finished] 0024962f test-mac-arm
[Failed] 0024962f ursa-i9-9960x
[Finished] 0024962f ursa-thinkcentre-m75q
[Finished] 03e80dc1 ec2-t3-xlarge-us-east-2
[Finished] 03e80dc1 test-mac-arm
[Failed] 03e80dc1 ursa-i9-9960x
[Finished] 03e80dc1 ursa-thinkcentre-m75q
Supported benchmarks:
ec2-t3-xlarge-us-east-2: Supported benchmark langs: Python, R. Runs only benchmarks with cloud = True
test-mac-arm: Supported benchmark langs: C++, Python, R
ursa-i9-9960x: Supported benchmark langs: Python, R, JavaScript
ursa-thinkcentre-m75q: Supported benchmark langs: C++, Java

@pitrou

Copy link
Copy Markdown
Member

@wjones127 They are in the nightly builds but not in the PR checks. Look for hdfs in archery docker images and you'll probably be able to reproduce locally :-)

pitrou pushed a commit that referenced this pull request Jul 19, 2022
…13615)
Follow up to #13577 / ARROW-17045.
Authored-by: Will Jones <willjones127@gmail.com>
Signed-off-by: Antoine Pitrou <antoine@python.org>
Tom-Newton added a commit to Tom-Newton/arrow that referenced this pull request Oct 16, 2023
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@wjones127@emkornfield@jorisvandenbossche@pitrou@nealrichardson@ursabot
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

ARROW-17045: [C++] Reject trailing slashes on file path - #13577

Merged
pitrou merged 7 commits into
apache:masterfrom
wjones127:ARROW-17045-gcs-slashes
Jul 13, 2022
Merged

ARROW-17045: [C++] Reject trailing slashes on file path#13577
pitrou merged 7 commits into
apache:masterfrom
wjones127:ARROW-17045-gcs-slashes

Conversation

@wjones127

@wjones127wjones127 commented Jul 11, 2022

Copy link
Copy Markdown
Member

BREAKING CHANGE: We had several different behaviors when passing in file paths with trailing slashes: LocalFileSystem would return IOError, S3 would trim off the trailing slash, and GCS would keep the trailing slash as part of the file name (later creating confusion as the file would be labelled a "directory" in list calls). This PR moves them all to the behavior of LocalFileSystem: return IOError.

The R filesystem bindings relied on the behavior provided by S3, since FileSystem$path() returns a SubTreeFileSystem as a convenient way to bundle a path and filesystem object and SubTreeFileSystem$base_path adds a trailing slash. To adapt to the C++ changes, the functions accepting SubTreeFileSystem as a path to a file now modified to trim the trailing slash before passing down to C++.

Here is an example of the differences in behavior between S3 and GCS:

importpyarrow.fsfrompyarrow.fsimportFileSelectorfromdatetimeimporttimedeltagcs=pyarrow.fs.GcsFileSystem(
endpoint_override="localhost:9001",
scheme="http",
anonymous=True,
retry_time_limit=timedelta(seconds=1),
)
gcs.create_dir("py_test")
# Writing to test.txt with and without slash produces a file and a directory!?withgcs.open_output_stream("py_test/test.txt") asout_stream:
out_stream.write(b"Hello world!")
withgcs.open_output_stream("py_test/test.txt/") asout_stream:
out_stream.write(b"Hello world!")
gcs.get_file_info(FileSelector("py_test"))
# [<FileInfo for 'py_test/test.txt': type=FileType.File, size=12>, <FileInfo for 'py_test/test.txt': type=FileType.Directory>]s3=pyarrow.fs.S3FileSystem(
access_key="minioadmin",
secret_key="minioadmin",
scheme="http",
endpoint_override="localhost:9000",
allow_bucket_creation=True,
allow_bucket_deletion=True,
)
s3.create_dir("py-test")
# Writing to test.txt with and without slash writes to same filewiths3.open_output_stream("py-test/test.txt") asout_stream:
out_stream.write(b"Hello world!")
withs3.open_output_stream("py-test/test.txt/") asout_stream:
out_stream.write(b"Hello world!")
s3.get_file_info(FileSelector("py-test"))
# [<FileInfo for 'py-test/test.txt': type=FileType.File, size=12>]

@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

⚠️ Ticket has not been started in JIRA, please click 'Start Progress'.

@wjones127

Copy link
Copy Markdown
MemberAuthor

cc @emkornfield@pitrou@coryan Does this behavior seem reasonable to you?

@emkornfield

Copy link
Copy Markdown
Contributor

Seems reasonable to me. Is it possible to test this against a generated dataset to make sure nothing breaks there?

@wjones127

Copy link
Copy Markdown
MemberAuthor

Seems reasonable to me. Is it possible to test this against a generated dataset to make sure nothing breaks there?

Do these tests from R seem sufficient? https://github.com/apache/arrow/blob/81af2912b5e088c83631df4cfd52e6d6df2878dc/r/tests/testthat/helper-filesystems.R#L130-L156

I pulled the changes from this PR into #13542, so the above tests will run in CI there. (I can confirm they pass locally for GCS and S3.)

@wjones127
wjones127 marked this pull request as ready for review July 12, 2022 04:55
@jorisvandenbossche

Copy link
Copy Markdown
Member

Just wondering: to what extent is this a corner case that just happens to be in our tests, or is there a practical value in allowing trailing slashes?

Because for example also the LocalFileSystem does not allow trailing slashes in a similar example:

In [51]: from pyarrow.fs import LocalFileSystem
In [52]: local = LocalFileSystem()
In [53]: local.create_dir("py-test")
In [54]: with local.open_output_stream("py-test/test.txt/") as out_stream:
...: out_stream.write(b"Hello world!")
...
IsADirectoryError: [Errno 21] Failed to open local file 'py-test/test.txt/'
Detail: [errno 21] Is a directory

(which is an error that makes sense to me, also Python's open will complain that it is a directory and not a file in the equivalent example)

@pitrou

Copy link
Copy Markdown
Member

I agree it seems better to disallow trailing slashes in filenames rather than silently dropping them.

@wjones127

Copy link
Copy Markdown
MemberAuthor

Just wondering: to what extent is this a corner case that just happens to be in our tests, or is there a practical value in allowing trailing slashes?

I'm not sure yet what else relies on that. In R, it's just that we pass fs$path("some/path") to functions like write_parquet(). fs$path() returns a SubTreeFilesystem, which I think is just used as a convenient way to pass a path (as the base path) and a filesystem in one object. But as a side effect of using base_path to transmit the path, we add a trailing slash. Using this method fails for LocalFileSystem though:

library(arrow)
#> #> Attaching package: 'arrow'#> The following object is masked from 'package:utils':#> #> timestampexample_data<- arrow_table(x=Array$create(c(1, 2, 3)))
fs<-LocalFileSystem$create()
write_parquet(example_data, fs$path("test.parquet"))
#> Error: IOError: Failed to open local file 'test.parquet/'#> /Users/willjones/Documents/arrows/arrow/cpp/src/arrow/filesystem/localfs.cc:442 ::arrow::internal::FileOpenWritable(fn, write_only, truncate, append). Detail: [errno 2] No such file or directory

Created on 2022-07-12 by the reprex package (v2.0.1)

cc @nealrichardson in case he has any input.

I will see about changing GCS to reject file paths that end with slashes. Should I leave S3 alone? Or should it also reject them?

@pitrou

Copy link
Copy Markdown
Member

Hmm, SubTreeFilesystem is meant to take a subdirectory parameter, you are not expected to use it to pass a filename.

@pitrou

Copy link
Copy Markdown
Member

Should I leave S3 alone? Or should it also reject them?

It would be fine with me to remove them, but if you hit too many regressions then no need to sweat over it either :-)

@nealrichardson

Copy link
Copy Markdown
Member

Hmm, SubTreeFilesystem is meant to take a subdirectory parameter, you are not expected to use it to pass a filename.

Historical context: ARROW-10254, which points to #8351 (comment)

We could have the single-file writers (perhaps the readers too) in R prune a possible trailing slash from filenames, if this is the only source of the issue. That logic is pretty well encapsulated in make_readable_file() and make_output_stream() so it should be feasible to do. Shouldn't be a concern for write_dataset/open_dataset since they point at directories.

Comment on lines +143 to +145
Status AssertNoTrailingSlash(const std::string& key) {
if (key.back() == '/') {
return NotAFile(key);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would take util::string_view instead but NotAFile only accepts const std::string&. Would it be alright if I moved "arrow/filesystem/util_internal.h" to use util::string_view?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely ok!

@wjones127wjones127 changed the title ARROW-17045: [C++] Ignore trailing slashes on files in GCSARROW-17045: [C++] Reject trailing slashes on file pathJul 12, 2022
Comment threadr/R/io.R Outdated
Comment threadr/R/io.R
file <- file$base_path
# SubTreeFileSystem adds a slash to base_path, but filesystems will reject file names
# with trailing slashes, so we need to remove it here.
file <- sub("/$", "", file$base_path)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm probably misreading this, but is this treating any SubTreeFileSystem as pointing to a local file path?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By "filesystems" I meant the Arrow FileSystem classes, not the local file system. Does that clarify your confusion? Or are you asking something else?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I am asking something else. It seems that this code is replacing file (a FileSystem instance) with a file path, is that right? And below, the file path file will be treated as a local filesystem path?

@wjones127wjones127Jul 13, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see. Yeah I just blindly updated this without thinking about what happens downstream 🤦

Below it will go through the code path where is.string(file) and !is.null(filesystem) are both TRUE, so it will later call file <- filesystem$OpenInputFile(file). So it went from SubTreeFilesystem to a CharacterVector to a <whatever OpenInputFile returns>. Wow that is some very dynamic typing 😵

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the path should be treated as correct filesystem since we extracted that out in the line before.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see, I had missed the filesystem <- file$base_fs. So I guess my last question is: why are we calling make_readable_file with a SubTreeFileSystem as the first argument?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expectation is that users will call FileSystem$path() to specify the location to write/read to:

fs<-S3FileSystem$create()
write_parquet(my_tab, fs$path("my/path/to"))

fs$path returns a SubTreeFileSystem as a convenient way to bundle a path and filesystem object in a single object. See discussion: #8351 (comment)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah. That's unfortunate, but that's not this PR's business. Thanks for the details!

@nealrichardsonnealrichardson left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R side LGTM, thanks!

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 from me, this is a reasonable straightening of the API. Thank you @wjones127 for doing this!

@pitrou
pitrou merged commit 0024962 into apache:masterJul 13, 2022
@wjones127
wjones127 deleted the ARROW-17045-gcs-slashes branch July 13, 2022 17:12
@jorisvandenbossche

Copy link
Copy Markdown
Member

There are some HDFS failures that might be related to this change? See eg https://github.com/ursacomputing/crossbow/runs/7331664487?check_suite_focus=true

@wjones127

Copy link
Copy Markdown
MemberAuthor

Oh no! Those do look related @jorisvandenbossche. Is HDFS not in our usual set of CI tests?

@ursabot

Copy link
Copy Markdown

Benchmark runs are scheduled for baseline = 03e80dc and contender = 0024962. 0024962 is a master commit associated with this PR. Results will be available as each benchmark for each run completes.
Conbench compare runs links:
[Finished ⬇️0.0% ⬆️0.0%] ec2-t3-xlarge-us-east-2
[Finished ⬇️0.75% ⬆️0.1%] test-mac-arm
[Failed ⬇️0.57% ⬆️0.0%] ursa-i9-9960x
[Finished ⬇️2.32% ⬆️0.11%] ursa-thinkcentre-m75q
Buildkite builds:
[Finished] 0024962f ec2-t3-xlarge-us-east-2
[Finished] 0024962f test-mac-arm
[Failed] 0024962f ursa-i9-9960x
[Finished] 0024962f ursa-thinkcentre-m75q
[Finished] 03e80dc1 ec2-t3-xlarge-us-east-2
[Finished] 03e80dc1 test-mac-arm
[Failed] 03e80dc1 ursa-i9-9960x
[Finished] 03e80dc1 ursa-thinkcentre-m75q
Supported benchmarks:
ec2-t3-xlarge-us-east-2: Supported benchmark langs: Python, R. Runs only benchmarks with cloud = True
test-mac-arm: Supported benchmark langs: C++, Python, R
ursa-i9-9960x: Supported benchmark langs: Python, R, JavaScript
ursa-thinkcentre-m75q: Supported benchmark langs: C++, Java

@pitrou

Copy link
Copy Markdown
Member

@wjones127 They are in the nightly builds but not in the PR checks. Look for hdfs in archery docker images and you'll probably be able to reproduce locally :-)

pitrou pushed a commit that referenced this pull request Jul 19, 2022
…13615)
Follow up to #13577 / ARROW-17045.
Authored-by: Will Jones <willjones127@gmail.com>
Signed-off-by: Antoine Pitrou <antoine@python.org>
Tom-Newton added a commit to Tom-Newton/arrow that referenced this pull request Oct 16, 2023
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@wjones127@emkornfield@jorisvandenbossche@pitrou@nealrichardson@ursabot
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

ARROW-17045: [C++] Reject trailing slashes on file path - #13577

Merged
pitrou merged 7 commits into
apache:masterfrom
wjones127:ARROW-17045-gcs-slashes
Jul 13, 2022
Merged

ARROW-17045: [C++] Reject trailing slashes on file path#13577
pitrou merged 7 commits into
apache:masterfrom
wjones127:ARROW-17045-gcs-slashes

Conversation

@wjones127

@wjones127wjones127 commented Jul 11, 2022

Copy link
Copy Markdown
Member

BREAKING CHANGE: We had several different behaviors when passing in file paths with trailing slashes: LocalFileSystem would return IOError, S3 would trim off the trailing slash, and GCS would keep the trailing slash as part of the file name (later creating confusion as the file would be labelled a "directory" in list calls). This PR moves them all to the behavior of LocalFileSystem: return IOError.

The R filesystem bindings relied on the behavior provided by S3, since FileSystem$path() returns a SubTreeFileSystem as a convenient way to bundle a path and filesystem object and SubTreeFileSystem$base_path adds a trailing slash. To adapt to the C++ changes, the functions accepting SubTreeFileSystem as a path to a file now modified to trim the trailing slash before passing down to C++.

Here is an example of the differences in behavior between S3 and GCS:

importpyarrow.fsfrompyarrow.fsimportFileSelectorfromdatetimeimporttimedeltagcs=pyarrow.fs.GcsFileSystem(
endpoint_override="localhost:9001",
scheme="http",
anonymous=True,
retry_time_limit=timedelta(seconds=1),
)
gcs.create_dir("py_test")
# Writing to test.txt with and without slash produces a file and a directory!?withgcs.open_output_stream("py_test/test.txt") asout_stream:
out_stream.write(b"Hello world!")
withgcs.open_output_stream("py_test/test.txt/") asout_stream:
out_stream.write(b"Hello world!")
gcs.get_file_info(FileSelector("py_test"))
# [<FileInfo for 'py_test/test.txt': type=FileType.File, size=12>, <FileInfo for 'py_test/test.txt': type=FileType.Directory>]s3=pyarrow.fs.S3FileSystem(
access_key="minioadmin",
secret_key="minioadmin",
scheme="http",
endpoint_override="localhost:9000",
allow_bucket_creation=True,
allow_bucket_deletion=True,
)
s3.create_dir("py-test")
# Writing to test.txt with and without slash writes to same filewiths3.open_output_stream("py-test/test.txt") asout_stream:
out_stream.write(b"Hello world!")
withs3.open_output_stream("py-test/test.txt/") asout_stream:
out_stream.write(b"Hello world!")
s3.get_file_info(FileSelector("py-test"))
# [<FileInfo for 'py-test/test.txt': type=FileType.File, size=12>]

@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

⚠️ Ticket has not been started in JIRA, please click 'Start Progress'.

@wjones127

Copy link
Copy Markdown
MemberAuthor

cc @emkornfield@pitrou@coryan Does this behavior seem reasonable to you?

@emkornfield

Copy link
Copy Markdown
Contributor

Seems reasonable to me. Is it possible to test this against a generated dataset to make sure nothing breaks there?

@wjones127

Copy link
Copy Markdown
MemberAuthor

Seems reasonable to me. Is it possible to test this against a generated dataset to make sure nothing breaks there?

Do these tests from R seem sufficient? https://github.com/apache/arrow/blob/81af2912b5e088c83631df4cfd52e6d6df2878dc/r/tests/testthat/helper-filesystems.R#L130-L156

I pulled the changes from this PR into #13542, so the above tests will run in CI there. (I can confirm they pass locally for GCS and S3.)

@wjones127
wjones127 marked this pull request as ready for review July 12, 2022 04:55
@jorisvandenbossche

Copy link
Copy Markdown
Member

Just wondering: to what extent is this a corner case that just happens to be in our tests, or is there a practical value in allowing trailing slashes?

Because for example also the LocalFileSystem does not allow trailing slashes in a similar example:

In [51]: from pyarrow.fs import LocalFileSystem
In [52]: local = LocalFileSystem()
In [53]: local.create_dir("py-test")
In [54]: with local.open_output_stream("py-test/test.txt/") as out_stream:
...: out_stream.write(b"Hello world!")
...
IsADirectoryError: [Errno 21] Failed to open local file 'py-test/test.txt/'
Detail: [errno 21] Is a directory

(which is an error that makes sense to me, also Python's open will complain that it is a directory and not a file in the equivalent example)

@pitrou

Copy link
Copy Markdown
Member

I agree it seems better to disallow trailing slashes in filenames rather than silently dropping them.

@wjones127

Copy link
Copy Markdown
MemberAuthor

Just wondering: to what extent is this a corner case that just happens to be in our tests, or is there a practical value in allowing trailing slashes?

I'm not sure yet what else relies on that. In R, it's just that we pass fs$path("some/path") to functions like write_parquet(). fs$path() returns a SubTreeFilesystem, which I think is just used as a convenient way to pass a path (as the base path) and a filesystem in one object. But as a side effect of using base_path to transmit the path, we add a trailing slash. Using this method fails for LocalFileSystem though:

library(arrow)
#> #> Attaching package: 'arrow'#> The following object is masked from 'package:utils':#> #> timestampexample_data<- arrow_table(x=Array$create(c(1, 2, 3)))
fs<-LocalFileSystem$create()
write_parquet(example_data, fs$path("test.parquet"))
#> Error: IOError: Failed to open local file 'test.parquet/'#> /Users/willjones/Documents/arrows/arrow/cpp/src/arrow/filesystem/localfs.cc:442 ::arrow::internal::FileOpenWritable(fn, write_only, truncate, append). Detail: [errno 2] No such file or directory

Created on 2022-07-12 by the reprex package (v2.0.1)

cc @nealrichardson in case he has any input.

I will see about changing GCS to reject file paths that end with slashes. Should I leave S3 alone? Or should it also reject them?

@pitrou

Copy link
Copy Markdown
Member

Hmm, SubTreeFilesystem is meant to take a subdirectory parameter, you are not expected to use it to pass a filename.

@pitrou

Copy link
Copy Markdown
Member

Should I leave S3 alone? Or should it also reject them?

It would be fine with me to remove them, but if you hit too many regressions then no need to sweat over it either :-)

@nealrichardson

Copy link
Copy Markdown
Member

Hmm, SubTreeFilesystem is meant to take a subdirectory parameter, you are not expected to use it to pass a filename.

Historical context: ARROW-10254, which points to #8351 (comment)

We could have the single-file writers (perhaps the readers too) in R prune a possible trailing slash from filenames, if this is the only source of the issue. That logic is pretty well encapsulated in make_readable_file() and make_output_stream() so it should be feasible to do. Shouldn't be a concern for write_dataset/open_dataset since they point at directories.

Comment on lines +143 to +145
Status AssertNoTrailingSlash(const std::string& key) {
if (key.back() == '/') {
return NotAFile(key);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would take util::string_view instead but NotAFile only accepts const std::string&. Would it be alright if I moved "arrow/filesystem/util_internal.h" to use util::string_view?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely ok!

@wjones127wjones127 changed the title ARROW-17045: [C++] Ignore trailing slashes on files in GCSARROW-17045: [C++] Reject trailing slashes on file pathJul 12, 2022
Comment threadr/R/io.R Outdated
Comment threadr/R/io.R
file <- file$base_path
# SubTreeFileSystem adds a slash to base_path, but filesystems will reject file names
# with trailing slashes, so we need to remove it here.
file <- sub("/$", "", file$base_path)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm probably misreading this, but is this treating any SubTreeFileSystem as pointing to a local file path?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By "filesystems" I meant the Arrow FileSystem classes, not the local file system. Does that clarify your confusion? Or are you asking something else?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I am asking something else. It seems that this code is replacing file (a FileSystem instance) with a file path, is that right? And below, the file path file will be treated as a local filesystem path?

@wjones127wjones127Jul 13, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see. Yeah I just blindly updated this without thinking about what happens downstream 🤦

Below it will go through the code path where is.string(file) and !is.null(filesystem) are both TRUE, so it will later call file <- filesystem$OpenInputFile(file). So it went from SubTreeFilesystem to a CharacterVector to a <whatever OpenInputFile returns>. Wow that is some very dynamic typing 😵

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the path should be treated as correct filesystem since we extracted that out in the line before.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see, I had missed the filesystem <- file$base_fs. So I guess my last question is: why are we calling make_readable_file with a SubTreeFileSystem as the first argument?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expectation is that users will call FileSystem$path() to specify the location to write/read to:

fs<-S3FileSystem$create()
write_parquet(my_tab, fs$path("my/path/to"))

fs$path returns a SubTreeFileSystem as a convenient way to bundle a path and filesystem object in a single object. See discussion: #8351 (comment)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah. That's unfortunate, but that's not this PR's business. Thanks for the details!

@nealrichardsonnealrichardson left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R side LGTM, thanks!

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 from me, this is a reasonable straightening of the API. Thank you @wjones127 for doing this!

@pitrou
pitrou merged commit 0024962 into apache:masterJul 13, 2022
@wjones127
wjones127 deleted the ARROW-17045-gcs-slashes branch July 13, 2022 17:12
@jorisvandenbossche

Copy link
Copy Markdown
Member

There are some HDFS failures that might be related to this change? See eg https://github.com/ursacomputing/crossbow/runs/7331664487?check_suite_focus=true

@wjones127

Copy link
Copy Markdown
MemberAuthor

Oh no! Those do look related @jorisvandenbossche. Is HDFS not in our usual set of CI tests?

@ursabot

Copy link
Copy Markdown

Benchmark runs are scheduled for baseline = 03e80dc and contender = 0024962. 0024962 is a master commit associated with this PR. Results will be available as each benchmark for each run completes.
Conbench compare runs links:
[Finished ⬇️0.0% ⬆️0.0%] ec2-t3-xlarge-us-east-2
[Finished ⬇️0.75% ⬆️0.1%] test-mac-arm
[Failed ⬇️0.57% ⬆️0.0%] ursa-i9-9960x
[Finished ⬇️2.32% ⬆️0.11%] ursa-thinkcentre-m75q
Buildkite builds:
[Finished] 0024962f ec2-t3-xlarge-us-east-2
[Finished] 0024962f test-mac-arm
[Failed] 0024962f ursa-i9-9960x
[Finished] 0024962f ursa-thinkcentre-m75q
[Finished] 03e80dc1 ec2-t3-xlarge-us-east-2
[Finished] 03e80dc1 test-mac-arm
[Failed] 03e80dc1 ursa-i9-9960x
[Finished] 03e80dc1 ursa-thinkcentre-m75q
Supported benchmarks:
ec2-t3-xlarge-us-east-2: Supported benchmark langs: Python, R. Runs only benchmarks with cloud = True
test-mac-arm: Supported benchmark langs: C++, Python, R
ursa-i9-9960x: Supported benchmark langs: Python, R, JavaScript
ursa-thinkcentre-m75q: Supported benchmark langs: C++, Java

@pitrou

Copy link
Copy Markdown
Member

@wjones127 They are in the nightly builds but not in the PR checks. Look for hdfs in archery docker images and you'll probably be able to reproduce locally :-)

pitrou pushed a commit that referenced this pull request Jul 19, 2022
…13615)
Follow up to #13577 / ARROW-17045.
Authored-by: Will Jones <willjones127@gmail.com>
Signed-off-by: Antoine Pitrou <antoine@python.org>
Tom-Newton added a commit to Tom-Newton/arrow that referenced this pull request Oct 16, 2023
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@wjones127@emkornfield@jorisvandenbossche@pitrou@nealrichardson@ursabot
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

ARROW-17045: [C++] Reject trailing slashes on file path - #13577

Merged
pitrou merged 7 commits into
apache:masterfrom
wjones127:ARROW-17045-gcs-slashes
Jul 13, 2022
Merged

ARROW-17045: [C++] Reject trailing slashes on file path#13577
pitrou merged 7 commits into
apache:masterfrom
wjones127:ARROW-17045-gcs-slashes

Conversation

@wjones127

@wjones127wjones127 commented Jul 11, 2022

Copy link
Copy Markdown
Member

BREAKING CHANGE: We had several different behaviors when passing in file paths with trailing slashes: LocalFileSystem would return IOError, S3 would trim off the trailing slash, and GCS would keep the trailing slash as part of the file name (later creating confusion as the file would be labelled a "directory" in list calls). This PR moves them all to the behavior of LocalFileSystem: return IOError.

The R filesystem bindings relied on the behavior provided by S3, since FileSystem$path() returns a SubTreeFileSystem as a convenient way to bundle a path and filesystem object and SubTreeFileSystem$base_path adds a trailing slash. To adapt to the C++ changes, the functions accepting SubTreeFileSystem as a path to a file now modified to trim the trailing slash before passing down to C++.

Here is an example of the differences in behavior between S3 and GCS:

importpyarrow.fsfrompyarrow.fsimportFileSelectorfromdatetimeimporttimedeltagcs=pyarrow.fs.GcsFileSystem(
endpoint_override="localhost:9001",
scheme="http",
anonymous=True,
retry_time_limit=timedelta(seconds=1),
)
gcs.create_dir("py_test")
# Writing to test.txt with and without slash produces a file and a directory!?withgcs.open_output_stream("py_test/test.txt") asout_stream:
out_stream.write(b"Hello world!")
withgcs.open_output_stream("py_test/test.txt/") asout_stream:
out_stream.write(b"Hello world!")
gcs.get_file_info(FileSelector("py_test"))
# [<FileInfo for 'py_test/test.txt': type=FileType.File, size=12>, <FileInfo for 'py_test/test.txt': type=FileType.Directory>]s3=pyarrow.fs.S3FileSystem(
access_key="minioadmin",
secret_key="minioadmin",
scheme="http",
endpoint_override="localhost:9000",
allow_bucket_creation=True,
allow_bucket_deletion=True,
)
s3.create_dir("py-test")
# Writing to test.txt with and without slash writes to same filewiths3.open_output_stream("py-test/test.txt") asout_stream:
out_stream.write(b"Hello world!")
withs3.open_output_stream("py-test/test.txt/") asout_stream:
out_stream.write(b"Hello world!")
s3.get_file_info(FileSelector("py-test"))
# [<FileInfo for 'py-test/test.txt': type=FileType.File, size=12>]

@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

⚠️ Ticket has not been started in JIRA, please click 'Start Progress'.

@wjones127

Copy link
Copy Markdown
MemberAuthor

cc @emkornfield@pitrou@coryan Does this behavior seem reasonable to you?

@emkornfield

Copy link
Copy Markdown
Contributor

Seems reasonable to me. Is it possible to test this against a generated dataset to make sure nothing breaks there?

@wjones127

Copy link
Copy Markdown
MemberAuthor

Seems reasonable to me. Is it possible to test this against a generated dataset to make sure nothing breaks there?

Do these tests from R seem sufficient? https://github.com/apache/arrow/blob/81af2912b5e088c83631df4cfd52e6d6df2878dc/r/tests/testthat/helper-filesystems.R#L130-L156

I pulled the changes from this PR into #13542, so the above tests will run in CI there. (I can confirm they pass locally for GCS and S3.)

@wjones127
wjones127 marked this pull request as ready for review July 12, 2022 04:55
@jorisvandenbossche

Copy link
Copy Markdown
Member

Just wondering: to what extent is this a corner case that just happens to be in our tests, or is there a practical value in allowing trailing slashes?

Because for example also the LocalFileSystem does not allow trailing slashes in a similar example:

In [51]: from pyarrow.fs import LocalFileSystem
In [52]: local = LocalFileSystem()
In [53]: local.create_dir("py-test")
In [54]: with local.open_output_stream("py-test/test.txt/") as out_stream:
...: out_stream.write(b"Hello world!")
...
IsADirectoryError: [Errno 21] Failed to open local file 'py-test/test.txt/'
Detail: [errno 21] Is a directory

(which is an error that makes sense to me, also Python's open will complain that it is a directory and not a file in the equivalent example)

@pitrou

Copy link
Copy Markdown
Member

I agree it seems better to disallow trailing slashes in filenames rather than silently dropping them.

@wjones127

Copy link
Copy Markdown
MemberAuthor

Just wondering: to what extent is this a corner case that just happens to be in our tests, or is there a practical value in allowing trailing slashes?

I'm not sure yet what else relies on that. In R, it's just that we pass fs$path("some/path") to functions like write_parquet(). fs$path() returns a SubTreeFilesystem, which I think is just used as a convenient way to pass a path (as the base path) and a filesystem in one object. But as a side effect of using base_path to transmit the path, we add a trailing slash. Using this method fails for LocalFileSystem though:

library(arrow)
#> #> Attaching package: 'arrow'#> The following object is masked from 'package:utils':#> #> timestampexample_data<- arrow_table(x=Array$create(c(1, 2, 3)))
fs<-LocalFileSystem$create()
write_parquet(example_data, fs$path("test.parquet"))
#> Error: IOError: Failed to open local file 'test.parquet/'#> /Users/willjones/Documents/arrows/arrow/cpp/src/arrow/filesystem/localfs.cc:442 ::arrow::internal::FileOpenWritable(fn, write_only, truncate, append). Detail: [errno 2] No such file or directory

Created on 2022-07-12 by the reprex package (v2.0.1)

cc @nealrichardson in case he has any input.

I will see about changing GCS to reject file paths that end with slashes. Should I leave S3 alone? Or should it also reject them?

@pitrou

Copy link
Copy Markdown
Member

Hmm, SubTreeFilesystem is meant to take a subdirectory parameter, you are not expected to use it to pass a filename.

@pitrou

Copy link
Copy Markdown
Member

Should I leave S3 alone? Or should it also reject them?

It would be fine with me to remove them, but if you hit too many regressions then no need to sweat over it either :-)

@nealrichardson

Copy link
Copy Markdown
Member

Hmm, SubTreeFilesystem is meant to take a subdirectory parameter, you are not expected to use it to pass a filename.

Historical context: ARROW-10254, which points to #8351 (comment)

We could have the single-file writers (perhaps the readers too) in R prune a possible trailing slash from filenames, if this is the only source of the issue. That logic is pretty well encapsulated in make_readable_file() and make_output_stream() so it should be feasible to do. Shouldn't be a concern for write_dataset/open_dataset since they point at directories.

Comment on lines +143 to +145
Status AssertNoTrailingSlash(const std::string& key) {
if (key.back() == '/') {
return NotAFile(key);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would take util::string_view instead but NotAFile only accepts const std::string&. Would it be alright if I moved "arrow/filesystem/util_internal.h" to use util::string_view?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely ok!

@wjones127wjones127 changed the title ARROW-17045: [C++] Ignore trailing slashes on files in GCSARROW-17045: [C++] Reject trailing slashes on file pathJul 12, 2022
Comment threadr/R/io.R Outdated
Comment threadr/R/io.R
file <- file$base_path
# SubTreeFileSystem adds a slash to base_path, but filesystems will reject file names
# with trailing slashes, so we need to remove it here.
file <- sub("/$", "", file$base_path)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm probably misreading this, but is this treating any SubTreeFileSystem as pointing to a local file path?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By "filesystems" I meant the Arrow FileSystem classes, not the local file system. Does that clarify your confusion? Or are you asking something else?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I am asking something else. It seems that this code is replacing file (a FileSystem instance) with a file path, is that right? And below, the file path file will be treated as a local filesystem path?

@wjones127wjones127Jul 13, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see. Yeah I just blindly updated this without thinking about what happens downstream 🤦

Below it will go through the code path where is.string(file) and !is.null(filesystem) are both TRUE, so it will later call file <- filesystem$OpenInputFile(file). So it went from SubTreeFilesystem to a CharacterVector to a <whatever OpenInputFile returns>. Wow that is some very dynamic typing 😵

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the path should be treated as correct filesystem since we extracted that out in the line before.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see, I had missed the filesystem <- file$base_fs. So I guess my last question is: why are we calling make_readable_file with a SubTreeFileSystem as the first argument?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expectation is that users will call FileSystem$path() to specify the location to write/read to:

fs<-S3FileSystem$create()
write_parquet(my_tab, fs$path("my/path/to"))

fs$path returns a SubTreeFileSystem as a convenient way to bundle a path and filesystem object in a single object. See discussion: #8351 (comment)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah. That's unfortunate, but that's not this PR's business. Thanks for the details!

@nealrichardsonnealrichardson left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R side LGTM, thanks!

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 from me, this is a reasonable straightening of the API. Thank you @wjones127 for doing this!

@pitrou
pitrou merged commit 0024962 into apache:masterJul 13, 2022
@wjones127
wjones127 deleted the ARROW-17045-gcs-slashes branch July 13, 2022 17:12
@jorisvandenbossche

Copy link
Copy Markdown
Member

There are some HDFS failures that might be related to this change? See eg https://github.com/ursacomputing/crossbow/runs/7331664487?check_suite_focus=true

@wjones127

Copy link
Copy Markdown
MemberAuthor

Oh no! Those do look related @jorisvandenbossche. Is HDFS not in our usual set of CI tests?

@ursabot

Copy link
Copy Markdown

Benchmark runs are scheduled for baseline = 03e80dc and contender = 0024962. 0024962 is a master commit associated with this PR. Results will be available as each benchmark for each run completes.
Conbench compare runs links:
[Finished ⬇️0.0% ⬆️0.0%] ec2-t3-xlarge-us-east-2
[Finished ⬇️0.75% ⬆️0.1%] test-mac-arm
[Failed ⬇️0.57% ⬆️0.0%] ursa-i9-9960x
[Finished ⬇️2.32% ⬆️0.11%] ursa-thinkcentre-m75q
Buildkite builds:
[Finished] 0024962f ec2-t3-xlarge-us-east-2
[Finished] 0024962f test-mac-arm
[Failed] 0024962f ursa-i9-9960x
[Finished] 0024962f ursa-thinkcentre-m75q
[Finished] 03e80dc1 ec2-t3-xlarge-us-east-2
[Finished] 03e80dc1 test-mac-arm
[Failed] 03e80dc1 ursa-i9-9960x
[Finished] 03e80dc1 ursa-thinkcentre-m75q
Supported benchmarks:
ec2-t3-xlarge-us-east-2: Supported benchmark langs: Python, R. Runs only benchmarks with cloud = True
test-mac-arm: Supported benchmark langs: C++, Python, R
ursa-i9-9960x: Supported benchmark langs: Python, R, JavaScript
ursa-thinkcentre-m75q: Supported benchmark langs: C++, Java

@pitrou

Copy link
Copy Markdown
Member

@wjones127 They are in the nightly builds but not in the PR checks. Look for hdfs in archery docker images and you'll probably be able to reproduce locally :-)

pitrou pushed a commit that referenced this pull request Jul 19, 2022
…13615)
Follow up to #13577 / ARROW-17045.
Authored-by: Will Jones <willjones127@gmail.com>
Signed-off-by: Antoine Pitrou <antoine@python.org>
Tom-Newton added a commit to Tom-Newton/arrow that referenced this pull request Oct 16, 2023
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@wjones127@emkornfield@jorisvandenbossche@pitrou@nealrichardson@ursabot
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

ARROW-17045: [C++] Reject trailing slashes on file path - #13577

Merged
pitrou merged 7 commits into
apache:masterfrom
wjones127:ARROW-17045-gcs-slashes
Jul 13, 2022
Merged

ARROW-17045: [C++] Reject trailing slashes on file path#13577
pitrou merged 7 commits into
apache:masterfrom
wjones127:ARROW-17045-gcs-slashes

Conversation

@wjones127

@wjones127wjones127 commented Jul 11, 2022

Copy link
Copy Markdown
Member

BREAKING CHANGE: We had several different behaviors when passing in file paths with trailing slashes: LocalFileSystem would return IOError, S3 would trim off the trailing slash, and GCS would keep the trailing slash as part of the file name (later creating confusion as the file would be labelled a "directory" in list calls). This PR moves them all to the behavior of LocalFileSystem: return IOError.

The R filesystem bindings relied on the behavior provided by S3, since FileSystem$path() returns a SubTreeFileSystem as a convenient way to bundle a path and filesystem object and SubTreeFileSystem$base_path adds a trailing slash. To adapt to the C++ changes, the functions accepting SubTreeFileSystem as a path to a file now modified to trim the trailing slash before passing down to C++.

Here is an example of the differences in behavior between S3 and GCS:

importpyarrow.fsfrompyarrow.fsimportFileSelectorfromdatetimeimporttimedeltagcs=pyarrow.fs.GcsFileSystem(
endpoint_override="localhost:9001",
scheme="http",
anonymous=True,
retry_time_limit=timedelta(seconds=1),
)
gcs.create_dir("py_test")
# Writing to test.txt with and without slash produces a file and a directory!?withgcs.open_output_stream("py_test/test.txt") asout_stream:
out_stream.write(b"Hello world!")
withgcs.open_output_stream("py_test/test.txt/") asout_stream:
out_stream.write(b"Hello world!")
gcs.get_file_info(FileSelector("py_test"))
# [<FileInfo for 'py_test/test.txt': type=FileType.File, size=12>, <FileInfo for 'py_test/test.txt': type=FileType.Directory>]s3=pyarrow.fs.S3FileSystem(
access_key="minioadmin",
secret_key="minioadmin",
scheme="http",
endpoint_override="localhost:9000",
allow_bucket_creation=True,
allow_bucket_deletion=True,
)
s3.create_dir("py-test")
# Writing to test.txt with and without slash writes to same filewiths3.open_output_stream("py-test/test.txt") asout_stream:
out_stream.write(b"Hello world!")
withs3.open_output_stream("py-test/test.txt/") asout_stream:
out_stream.write(b"Hello world!")
s3.get_file_info(FileSelector("py-test"))
# [<FileInfo for 'py-test/test.txt': type=FileType.File, size=12>]

@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

⚠️ Ticket has not been started in JIRA, please click 'Start Progress'.

@wjones127

Copy link
Copy Markdown
MemberAuthor

cc @emkornfield@pitrou@coryan Does this behavior seem reasonable to you?

@emkornfield

Copy link
Copy Markdown
Contributor

Seems reasonable to me. Is it possible to test this against a generated dataset to make sure nothing breaks there?

@wjones127

Copy link
Copy Markdown
MemberAuthor

Seems reasonable to me. Is it possible to test this against a generated dataset to make sure nothing breaks there?

Do these tests from R seem sufficient? https://github.com/apache/arrow/blob/81af2912b5e088c83631df4cfd52e6d6df2878dc/r/tests/testthat/helper-filesystems.R#L130-L156

I pulled the changes from this PR into #13542, so the above tests will run in CI there. (I can confirm they pass locally for GCS and S3.)

@wjones127
wjones127 marked this pull request as ready for review July 12, 2022 04:55
@jorisvandenbossche

Copy link
Copy Markdown
Member

Just wondering: to what extent is this a corner case that just happens to be in our tests, or is there a practical value in allowing trailing slashes?

Because for example also the LocalFileSystem does not allow trailing slashes in a similar example:

In [51]: from pyarrow.fs import LocalFileSystem
In [52]: local = LocalFileSystem()
In [53]: local.create_dir("py-test")
In [54]: with local.open_output_stream("py-test/test.txt/") as out_stream:
...: out_stream.write(b"Hello world!")
...
IsADirectoryError: [Errno 21] Failed to open local file 'py-test/test.txt/'
Detail: [errno 21] Is a directory

(which is an error that makes sense to me, also Python's open will complain that it is a directory and not a file in the equivalent example)

@pitrou

Copy link
Copy Markdown
Member

I agree it seems better to disallow trailing slashes in filenames rather than silently dropping them.

@wjones127

Copy link
Copy Markdown
MemberAuthor

Just wondering: to what extent is this a corner case that just happens to be in our tests, or is there a practical value in allowing trailing slashes?

I'm not sure yet what else relies on that. In R, it's just that we pass fs$path("some/path") to functions like write_parquet(). fs$path() returns a SubTreeFilesystem, which I think is just used as a convenient way to pass a path (as the base path) and a filesystem in one object. But as a side effect of using base_path to transmit the path, we add a trailing slash. Using this method fails for LocalFileSystem though:

library(arrow)
#> #> Attaching package: 'arrow'#> The following object is masked from 'package:utils':#> #> timestampexample_data<- arrow_table(x=Array$create(c(1, 2, 3)))
fs<-LocalFileSystem$create()
write_parquet(example_data, fs$path("test.parquet"))
#> Error: IOError: Failed to open local file 'test.parquet/'#> /Users/willjones/Documents/arrows/arrow/cpp/src/arrow/filesystem/localfs.cc:442 ::arrow::internal::FileOpenWritable(fn, write_only, truncate, append). Detail: [errno 2] No such file or directory

Created on 2022-07-12 by the reprex package (v2.0.1)

cc @nealrichardson in case he has any input.

I will see about changing GCS to reject file paths that end with slashes. Should I leave S3 alone? Or should it also reject them?

@pitrou

Copy link
Copy Markdown
Member

Hmm, SubTreeFilesystem is meant to take a subdirectory parameter, you are not expected to use it to pass a filename.

@pitrou

Copy link
Copy Markdown
Member

Should I leave S3 alone? Or should it also reject them?

It would be fine with me to remove them, but if you hit too many regressions then no need to sweat over it either :-)

@nealrichardson

Copy link
Copy Markdown
Member

Hmm, SubTreeFilesystem is meant to take a subdirectory parameter, you are not expected to use it to pass a filename.

Historical context: ARROW-10254, which points to #8351 (comment)

We could have the single-file writers (perhaps the readers too) in R prune a possible trailing slash from filenames, if this is the only source of the issue. That logic is pretty well encapsulated in make_readable_file() and make_output_stream() so it should be feasible to do. Shouldn't be a concern for write_dataset/open_dataset since they point at directories.

Comment on lines +143 to +145
Status AssertNoTrailingSlash(const std::string& key) {
if (key.back() == '/') {
return NotAFile(key);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would take util::string_view instead but NotAFile only accepts const std::string&. Would it be alright if I moved "arrow/filesystem/util_internal.h" to use util::string_view?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely ok!

@wjones127wjones127 changed the title ARROW-17045: [C++] Ignore trailing slashes on files in GCSARROW-17045: [C++] Reject trailing slashes on file pathJul 12, 2022
Comment threadr/R/io.R Outdated
Comment threadr/R/io.R
file <- file$base_path
# SubTreeFileSystem adds a slash to base_path, but filesystems will reject file names
# with trailing slashes, so we need to remove it here.
file <- sub("/$", "", file$base_path)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm probably misreading this, but is this treating any SubTreeFileSystem as pointing to a local file path?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By "filesystems" I meant the Arrow FileSystem classes, not the local file system. Does that clarify your confusion? Or are you asking something else?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I am asking something else. It seems that this code is replacing file (a FileSystem instance) with a file path, is that right? And below, the file path file will be treated as a local filesystem path?

@wjones127wjones127Jul 13, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see. Yeah I just blindly updated this without thinking about what happens downstream 🤦

Below it will go through the code path where is.string(file) and !is.null(filesystem) are both TRUE, so it will later call file <- filesystem$OpenInputFile(file). So it went from SubTreeFilesystem to a CharacterVector to a <whatever OpenInputFile returns>. Wow that is some very dynamic typing 😵

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the path should be treated as correct filesystem since we extracted that out in the line before.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see, I had missed the filesystem <- file$base_fs. So I guess my last question is: why are we calling make_readable_file with a SubTreeFileSystem as the first argument?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expectation is that users will call FileSystem$path() to specify the location to write/read to:

fs<-S3FileSystem$create()
write_parquet(my_tab, fs$path("my/path/to"))

fs$path returns a SubTreeFileSystem as a convenient way to bundle a path and filesystem object in a single object. See discussion: #8351 (comment)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah. That's unfortunate, but that's not this PR's business. Thanks for the details!

@nealrichardsonnealrichardson left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R side LGTM, thanks!

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 from me, this is a reasonable straightening of the API. Thank you @wjones127 for doing this!

@pitrou
pitrou merged commit 0024962 into apache:masterJul 13, 2022
@wjones127
wjones127 deleted the ARROW-17045-gcs-slashes branch July 13, 2022 17:12
@jorisvandenbossche

Copy link
Copy Markdown
Member

There are some HDFS failures that might be related to this change? See eg https://github.com/ursacomputing/crossbow/runs/7331664487?check_suite_focus=true

@wjones127

Copy link
Copy Markdown
MemberAuthor

Oh no! Those do look related @jorisvandenbossche. Is HDFS not in our usual set of CI tests?

@ursabot

Copy link
Copy Markdown

Benchmark runs are scheduled for baseline = 03e80dc and contender = 0024962. 0024962 is a master commit associated with this PR. Results will be available as each benchmark for each run completes.
Conbench compare runs links:
[Finished ⬇️0.0% ⬆️0.0%] ec2-t3-xlarge-us-east-2
[Finished ⬇️0.75% ⬆️0.1%] test-mac-arm
[Failed ⬇️0.57% ⬆️0.0%] ursa-i9-9960x
[Finished ⬇️2.32% ⬆️0.11%] ursa-thinkcentre-m75q
Buildkite builds:
[Finished] 0024962f ec2-t3-xlarge-us-east-2
[Finished] 0024962f test-mac-arm
[Failed] 0024962f ursa-i9-9960x
[Finished] 0024962f ursa-thinkcentre-m75q
[Finished] 03e80dc1 ec2-t3-xlarge-us-east-2
[Finished] 03e80dc1 test-mac-arm
[Failed] 03e80dc1 ursa-i9-9960x
[Finished] 03e80dc1 ursa-thinkcentre-m75q
Supported benchmarks:
ec2-t3-xlarge-us-east-2: Supported benchmark langs: Python, R. Runs only benchmarks with cloud = True
test-mac-arm: Supported benchmark langs: C++, Python, R
ursa-i9-9960x: Supported benchmark langs: Python, R, JavaScript
ursa-thinkcentre-m75q: Supported benchmark langs: C++, Java

@pitrou

Copy link
Copy Markdown
Member

@wjones127 They are in the nightly builds but not in the PR checks. Look for hdfs in archery docker images and you'll probably be able to reproduce locally :-)

pitrou pushed a commit that referenced this pull request Jul 19, 2022
…13615)
Follow up to #13577 / ARROW-17045.
Authored-by: Will Jones <willjones127@gmail.com>
Signed-off-by: Antoine Pitrou <antoine@python.org>
Tom-Newton added a commit to Tom-Newton/arrow that referenced this pull request Oct 16, 2023
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@wjones127@emkornfield@jorisvandenbossche@pitrou@nealrichardson@ursabot