[feature] Add `--resolve-symbolic-links` flag
This PR fixes #160
Design Overview (link.py)
link.py implements the new layer around two small internal abstractions:
1) _FileVersion – “physical” file instances
_FileVersion binds an underlying MountSource path/version to a union path and additionally stores a physical parent chain (parent: Optional[_FileVersion]). This is used to resolve relative links containing .. deterministically. The code explicitly frames this as analogous to lexical scoping: the meaning of .. is tied to the link’s static location in the underlying hierarchy rather than the access path in the merged tree.
2) _UnionPath – “logical” merged paths
_UnionPath represents a path in the merged view and may correspond to multiple _FileVersions. It provides:
-
Direct and transitive link expansion with deduplication to avoid cycles (
deduplicated_transitive_link_targets). - Separation of resolved folder vs resolved non-folder candidates.
- A child-lookup strategy described as analogous to dynamic dispatch: children are discovered by searching across all resolved folder versions that constitute the union path.
3) Late-binding semantics
When shouldResolveLink(linkname, file_type) returns true, symlink/hardlink targets are resolved within the union view, not constrained to the original source. This provides the intended “late binding” behavior for layered trees (e.g., merging older archives with newer on-disk updates).
4) Versioning and synthetic directories
To preserve the existing versioned-lookup model, the layer presents a synthetic directory entry as version 0 when any resolved folder versions exist, with file versions shifted accordingly. This allows directory merging while keeping the version API coherent for callers.
Refactor: MultiMountSourceMixin (multi.py)
This PR introduces MultiMountSourceMixin and moves common multi-source patterns out of UnionMountSource. The mixin centralizes the standard userdata delegation protocol:
- store delegated mount source in
fileInfo.userdata - pop on operation
- restore afterward
It also consolidates merged statfs, context management using ExitStack, and join_threads handling for underlying sources. This keeps UnionMountSource and LinkResolutionUnionMountSource aligned and reduces maintenance risk as more compositing layers are added.
UnionMountSource now simply inherits this mixin.
Behavior Changes
- Default behavior remains unchanged unless the new flag/layer is enabled.
- When enabled, symlink/hardlink resolution in union mounts becomes consistent across mount order and matches the policy specified by
shouldResolveLink.
Sorry I did not have time to continue working on this PR recently. I did not mean to abandon it. Just need some more time.
I just updated the PR as discussed in #160. @mxmlnkn would you mind enabling GitHub Actions for this PR?
I use Claude Code to write some tests. Would you mind using AI? @mxmlnkn
I think this is a relatively safe PR, because it does not change current behavior by default.
@mxmlnkn Could you approve the GitHub Actions so we could see how the workflow runs?
It may be safe, but it is huge. That is a barrier to reviewing. And approving the CI can have dangers for non-reviewed code. Note that you can enable the CI on your own fork for testing purposes. Or you can run tests/runtests.sh from the git root locally.
@mxmlnkn Although this PR looks large in terms of lines, the changes are focused on a single feature and the logic is straightforward. I have already run the GitHub Actions workflow on my fork (https://github.com/Atry/ratarmount/actions/runs/19939824034), and all tests passed successfully.
This PR fixes #160
Design Overview (link.py)
link.pyimplements the new layer around two small internal abstractions:1)
_FileVersion– “physical” file instances
_FileVersionbinds an underlyingMountSourcepath/version to a union path and additionally stores a physical parent chain (parent: Optional[_FileVersion]). This is used to resolve relative links containing..deterministically. The code explicitly frames this as analogous to lexical scoping: the meaning of..is tied to the link’s static location in the underlying hierarchy rather than the access path in the merged tree.2)
_UnionPath– “logical” merged paths
_UnionPathrepresents a path in the merged view and may correspond to multiple_FileVersions. It provides:* **Direct and transitive link expansion** with deduplication to avoid cycles (`deduplicated_transitive_link_targets`). * Separation of **resolved folder** vs **resolved non-folder** candidates. * A child-lookup strategy described as analogous to **dynamic dispatch**: children are discovered by searching across all resolved folder versions that constitute the union path.3) Late-binding semantics
When
shouldResolveLink(linkname, file_type)returns true, symlink/hardlink targets are resolved within the union view, not constrained to the original source. This provides the intended “late binding” behavior for layered trees (e.g., merging older archives with newer on-disk updates).4) Versioning and synthetic directories
To preserve the existing versioned-lookup model, the layer presents a synthetic directory entry as version
0when any resolved folder versions exist, with file versions shifted accordingly. This allows directory merging while keeping the version API coherent for callers.Refactor: MultiMountSourceMixin (multi.py)
This PR introduces
MultiMountSourceMixinand moves common multi-source patterns out ofUnionMountSource. The mixin centralizes the standard userdata delegation protocol:1. store delegated mount source in `fileInfo.userdata` 2. pop on operation 3. restore afterwardIt also consolidates merged
statfs, context management usingExitStack, andjoin_threadshandling for underlying sources. This keepsUnionMountSourceandLinkResolutionUnionMountSourcealigned and reduces maintenance risk as more compositing layers are added.
UnionMountSourcenow simply inherits this mixin.Behavior Changes
* Default behavior remains unchanged unless the new flag/layer is enabled. * When enabled, symlink/hardlink resolution in union mounts becomes **consistent across mount order** and matches the policy specified by `shouldResolveLink`.
Nice design overview. This helps.
I also changed the implementation of union mount point's __exit__. The original __exit__ implementation can skip an underlying mount point's __exit__ when an exception is raised during another underlying mount point's __exit__. ExitStack is a more sophisticated utility to handle this case.