Skip to content

Commit

Permalink
Treat data links even more like symlinks, allow crossing using / (#…
Browse files Browse the repository at this point in the history
…11926)

- Closes #11825
  • Loading branch information
radeusgd authored Jan 8, 2025
1 parent 25933af commit 8c900d5
Show file tree
Hide file tree
Showing 26 changed files with 826 additions and 334 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@
[11902]: https://github.com/enso-org/enso/pull/11902
[11908]: https://github.com/enso-org/enso/pull/11908

#### Enso Standard Library

- [Allow using `/` to access files inside a directory reached through a data
link.][11926]

[11926]: https://github.com/enso-org/enso/pull/11926

#### Enso Language & Runtime

- [Promote broken values instead of ignoring them][11777].
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
private

from Standard.Base import all
import Standard.Base.Errors.Illegal_Argument.Illegal_Argument
import Standard.Base.Internal.Path_Helpers

import project.Internal.S3_Path.S3_Path

type Path_Entry
Directory (name : Text)

File (name : Text)

is_directory self -> Boolean = case self of
Path_Entry.Directory _ -> True
Path_Entry.File _ -> False

type Decomposed_S3_Path
Value (parts : Vector Path_Entry)

## Reconstructs the original path.
key self -> Text =
add_directory_suffix = self.parts.not_empty && self.parts.last.is_directory
suffix = if add_directory_suffix then S3_Path.delimiter else ""
self.parts.map .name . join separator=S3_Path.delimiter suffix=suffix

parse (key : Text) -> Decomposed_S3_Path =
has_directory_suffix = key.ends_with S3_Path.delimiter
parts = key.split S3_Path.delimiter . filter (p-> p.is_empty.not)
entries = case has_directory_suffix of
True -> parts.map Path_Entry.Directory
False ->
if parts.is_empty then [] else
(parts.drop (..Last 1) . map Path_Entry.Directory) + [Path_Entry.File parts.last]
Decomposed_S3_Path.Value entries

join (paths : Vector Decomposed_S3_Path) -> Decomposed_S3_Path =
if paths.is_empty then Error.throw (Illegal_Argument.Error "Cannot join an empty list of paths.") else
flattened = paths.flat_map .parts
# Any `File` parts from the middle are now transformed to `Directory`:
aligned = flattened.map_with_index ix-> part-> case part of
Path_Entry.Directory _ -> part
Path_Entry.File name ->
is_last = ix == flattened.length-1
if is_last then part else Path_Entry.Directory name
Decomposed_S3_Path.Value aligned

normalize self -> Decomposed_S3_Path =
new_parts = Path_Helpers.normalize_segments self.parts .name
Decomposed_S3_Path.Value new_parts

parent self -> Decomposed_S3_Path | Nothing =
if self.parts.is_empty then Nothing else
new_parts = self.parts.drop (..Last 1)
Decomposed_S3_Path.Value new_parts

is_empty self -> Boolean = self.parts.is_empty

first_part self -> Path_Entry | Nothing =
if self.parts.is_empty then Nothing else
self.parts.first

drop_first_part self -> Decomposed_S3_Path =
if self.parts.is_empty then self else
new_parts = self.parts.drop 1
Decomposed_S3_Path.Value new_parts
88 changes: 21 additions & 67 deletions distribution/lib/Standard/AWS/0.0.0-dev/src/Internal/S3_Path.enso
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from Standard.Base import all
import Standard.Base.Errors.Illegal_Argument.Illegal_Argument
import Standard.Base.Internal.Path_Helpers

import project.Errors.S3_Error
import project.Internal.Decomposed_S3_Path.Decomposed_S3_Path
import project.S3.S3

## PRIVATE
Expand All @@ -25,8 +25,7 @@ type S3_Path
bucket = (without_prefix.take first_slash_index)
if bucket == "" then Error.throw (Illegal_Argument.Error "Invalid S3 path: empty bucket name with key name.") else
key = (without_prefix.drop first_slash_index+1)
normalized = Decomposed_S3_Path.parse key . normalize . key
S3_Path.Value bucket normalized
S3_Path.Value bucket key

## PRIVATE
to_text self -> Text =
Expand All @@ -43,6 +42,20 @@ type S3_Path
Checks if this path represents a directory.
is_directory self -> Boolean = self.is_root || (self.key.ends_with S3_Path.delimiter)

## PRIVATE
private set_new_path self new_path:Decomposed_S3_Path -> S3_Path =
# Handle the edge case of resolving `s3://` path without bucket - first part of the key becomes the bucket name
has_no_bucket = self.bucket == ""
set_new_bucket = has_no_bucket && new_path.is_empty.not
case set_new_bucket of
True ->
new_bucket = new_path.first_part.name
new_key = new_path.drop_first_part.normalize.key
S3_Path.Value new_bucket new_key
False ->
new_key = new_path.normalize.key
S3_Path.Value self.bucket new_key

## PRIVATE
Resolves a subdirectory entry.
This only makes logical sense for paths for which `path.is_directory == True`,
Expand All @@ -52,15 +65,12 @@ type S3_Path
if `subpath` ends with the delimiter.
resolve self (subpath : Text) -> S3_Path =
joined = Decomposed_S3_Path.join [Decomposed_S3_Path.parse self.key, Decomposed_S3_Path.parse subpath]
new_key = joined.normalize.key
S3_Path.Value self.bucket new_key
self.set_new_path joined

## PRIVATE
join self (subpaths : Vector) -> S3_Path =
joined = Decomposed_S3_Path.join (([self.key]+subpaths).map Decomposed_S3_Path.parse)
new_key = joined.normalize.key
S3_Path.Value self.bucket new_key

self.set_new_path joined

## PRIVATE
Returns the parent directory.
Expand Down Expand Up @@ -94,65 +104,9 @@ type S3_Path
path delimiter. In the future we could allow customizing it.
delimiter -> Text = "/"

## PRIVATE
type Path_Entry
## PRIVATE
Directory (name : Text)

## PRIVATE
File (name : Text)

## PRIVATE
is_directory self -> Boolean = case self of
Path_Entry.Directory _ -> True
Path_Entry.File _ -> False

## PRIVATE
type Decomposed_S3_Path
## PRIVATE
Value (parts : Vector Path_Entry) (go_to_root : Boolean)

## PRIVATE
Reconstructs the original path.
key self -> Text =
add_directory_suffix = self.parts.not_empty && self.parts.last.is_directory
suffix = if add_directory_suffix then S3_Path.delimiter else ""
self.parts.map .name . join separator=S3_Path.delimiter suffix=suffix

## PRIVATE
parse (key : Text) -> Decomposed_S3_Path =
has_directory_suffix = key.ends_with S3_Path.delimiter
has_root_prefix = key.starts_with S3_Path.delimiter
parts = key.split S3_Path.delimiter . filter (p-> p.is_empty.not)
entries = case has_directory_suffix of
True -> parts.map Path_Entry.Directory
False ->
if parts.is_empty then [] else
(parts.drop (..Last 1) . map Path_Entry.Directory) + [Path_Entry.File parts.last]
Decomposed_S3_Path.Value entries has_root_prefix

## PRIVATE
join (paths : Vector Decomposed_S3_Path) -> Decomposed_S3_Path =
if paths.is_empty then Error.throw (Illegal_Argument.Error "Cannot join an empty list of paths.") else
last_root_ix = paths.last_index_of (.go_to_root)
without_ignored_paths = if last_root_ix.is_nothing then paths else
paths.drop last_root_ix
flattened = without_ignored_paths.flat_map .parts
# Any `File` parts from the middle are now transformed to `Directory`:
aligned = flattened.map_with_index ix-> part-> case part of
Path_Entry.Directory _ -> part
Path_Entry.File name ->
is_last = ix == flattened.length-1
if is_last then part else Path_Entry.Directory name
Decomposed_S3_Path.Value aligned (last_root_ix.is_nothing.not)

## PRIVATE
normalize self -> Decomposed_S3_Path ! S3_Error =
new_parts = Path_Helpers.normalize_segments self.parts .name
Decomposed_S3_Path.Value new_parts self.go_to_root
bucket_root self -> S3_Path = S3_Path.Value self.bucket ""

## PRIVATE
parent self -> Decomposed_S3_Path | Nothing =
if self.parts.is_empty then Nothing else
new_parts = self.parts.drop (..Last 1)
Decomposed_S3_Path.Value new_parts self.go_to_root
without_trailing_slash self -> S3_Path =
if self.key.ends_with S3_Path.delimiter then S3_Path.Value self.bucket (self.key.drop (..Last 1)) else self
Loading

0 comments on commit 8c900d5

Please sign in to comment.