aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_partial_uploads.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/tests/test_partial_uploads.py')
-rw-r--r--packages/meshbay-node/tests/test_partial_uploads.py118
1 files changed, 107 insertions, 11 deletions
diff --git a/packages/meshbay-node/tests/test_partial_uploads.py b/packages/meshbay-node/tests/test_partial_uploads.py
index 80ab454..52aa306 100644
--- a/packages/meshbay-node/tests/test_partial_uploads.py
+++ b/packages/meshbay-node/tests/test_partial_uploads.py
@@ -17,10 +17,12 @@ may inherit — or overwrite the position of — the other's.
"""
import os
+import re
import time
import types
from pathlib import Path
+import pytest
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from meshbay_common.crypto import generate_gek
from meshbay_common.protocol import (
@@ -104,7 +106,7 @@ def _old(seconds: float) -> float:
NOW = 1_000_000.0
-FILM = Path("/roots/media/film.mkv.part")
+FILM = Path("/roots/media/film.mkv.0123abcd.part")
def test_a_part_nobody_is_writing_and_nobody_has_touched_is_deleted():
@@ -138,7 +140,7 @@ def test_the_same_name_in_another_directory_does_not_protect_it():
path rebuilt from a root and a relative directory would be a second
implementation that has to agree with the first for ever, and the state
records the path it is writing instead."""
- other = Path("/roots/archive/film.mkv.part")
+ other = Path("/roots/archive/film.mkv.0123abcd.part")
doomed = orphaned_parts([(other, _old(ORPHAN_AFTER_SECS + 1))],
live={FILM}, now=NOW)
assert doomed == [other]
@@ -157,15 +159,15 @@ def test_a_finished_file_is_not_a_candidate():
def test_a_file_from_the_future_is_left_alone():
"""A clock that went backwards is not evidence that a file is abandoned, and
deleting is not reversible."""
- doomed = orphaned_parts([(Path("/roots/media/a.part"), NOW + 10_000)],
+ doomed = orphaned_parts([(Path("/roots/media/a.0123abcd.part"), NOW + 10_000)],
live=set(), now=NOW)
assert doomed == []
def test_the_boundary_is_the_age_itself():
- at = [(Path("/roots/media/a.part"), _old(ORPHAN_AFTER_SECS))]
- just_under = [(Path("/roots/media/a.part"), _old(ORPHAN_AFTER_SECS - 1))]
- assert orphaned_parts(at, set(), NOW) == [Path("/roots/media/a.part")]
+ at = [(Path("/roots/media/a.0123abcd.part"), _old(ORPHAN_AFTER_SECS))]
+ just_under = [(Path("/roots/media/a.0123abcd.part"), _old(ORPHAN_AFTER_SECS - 1))]
+ assert orphaned_parts(at, set(), NOW) == [Path("/roots/media/a.0123abcd.part")]
assert orphaned_parts(just_under, set(), NOW) == []
@@ -243,9 +245,9 @@ def test_the_janitor_deletes_the_abandoned_and_keeps_the_rest(tmp_path):
one somebody is still writing stay, and a finished file is never a
candidate."""
root = _root(tmp_path, "media")
- old = _aged(root.path / "abandoned.mkv.part", ORPHAN_AFTER_SECS + 60)
- recent = _aged(root.path / "fresh.mkv.part", 30)
- live = _aged(root.path / "sending.mkv.part", ORPHAN_AFTER_SECS * 2)
+ old = _aged(root.path / "abandoned.mkv.0123abcd.part", ORPHAN_AFTER_SECS + 60)
+ recent = _aged(root.path / "fresh.mkv.0123abcd.part", 30)
+ live = _aged(root.path / "sending.mkv.0123abcd.part", ORPHAN_AFTER_SECS * 2)
finished = _aged(root.path / "done.mkv", ORPHAN_AFTER_SECS * 5)
uploads = PartialUploads()
@@ -262,7 +264,7 @@ def test_a_group_that_has_never_uploaded_anything_is_handled(tmp_path):
"""No `partial_uploads` in the context yet — it is created on first use, so
a node that has been up for five minutes has none."""
root = _root(tmp_path, "media")
- old = _aged(root.path / "left.mkv.part", ORPHAN_AFTER_SECS + 1)
+ old = _aged(root.path / "left.mkv.0123abcd.part", ORPHAN_AFTER_SECS + 1)
daemon = _daemon({"g1": {"roots": RootSet(roots=[root])}})
assert daemon._reap_once() == 1
assert not old.exists()
@@ -362,7 +364,7 @@ async def test_an_upload_in_flight_is_known_to_the_reaper(tmp_path):
total_chunks=2))
live = ctx["partial_uploads"].live_paths()
assert len(live) == 1
- assert next(iter(live)).name == "film.mkv.part"
+ assert re.fullmatch(r"film\.mkv\.[0-9a-f]{8}\.part", next(iter(live)).name)
assert next(iter(live)).exists()
@@ -489,3 +491,97 @@ async def test_an_upload_chunk_says_its_slot_is_in_use(tmp_path):
assert _errors(peer) == []
assert slots.leases["up-1"].used is True, (
"the node still believes nobody took this slot up, and will reclaim it")
+
+
+
+# ── never replacing a file ──────────────────────────────────────────────────
+
+async def test_two_members_sending_one_name_at_once_get_two_files(tmp_path):
+ """
+ Both chose a free name at chunk 0, and the free name was the same: the
+ final file did not exist yet, only the first one's `.part`. They then wrote
+ one `.part`, the second truncating the first, and published it twice.
+ """
+ ctx = _group_ctx(tmp_path)
+ alice, bob = _peer(ctx, "alice"), _peer(ctx, "bob")
+ for who, data in ((alice, b"hers-1"), (bob, b"his-1")):
+ await who._do_file_upload(sealed_upload(who, filename="IMG_1234.jpg", data=data,
+ chunk_index=0, total_chunks=2))
+ for who, data in ((alice, b"hers-2"), (bob, b"his-2")):
+ await who._do_file_upload(sealed_upload(who, filename="IMG_1234.jpg", data=data,
+ chunk_index=1, total_chunks=2))
+ assert _errors(alice) == [] and _errors(bob) == []
+
+ root = ctx["roots"].roots[0].path
+ assert (root / "IMG_1234.jpg").read_bytes() == b"hers-1hers-2"
+ assert (root / "IMG_1234 (2).jpg").read_bytes() == b"his-1his-2"
+ assert _acks(bob, ctx)[-1]["stored_as"] == "IMG_1234 (2).jpg"
+
+
+async def test_a_file_that_appears_during_an_upload_is_not_replaced(tmp_path):
+ """The operator copies a file in under the same name while a member's
+ upload is running. The rename at the end used to replace it."""
+ ctx = _group_ctx(tmp_path)
+ peer = _peer(ctx)
+ await peer._do_file_upload(sealed_upload(peer, filename="film.mkv", data=b"up-1",
+ chunk_index=0, total_chunks=2))
+ root = ctx["roots"].roots[0].path
+ (root / "film.mkv").write_bytes(b"the operator's")
+ await peer._do_file_upload(sealed_upload(peer, filename="film.mkv", data=b"up-2",
+ chunk_index=1, total_chunks=2))
+
+ assert _errors(peer) == []
+ assert (root / "film.mkv").read_bytes() == b"the operator's"
+ assert (root / "film (2).mkv").read_bytes() == b"up-1up-2"
+ assert _acks(peer, ctx)[-1]["stored_as"] == "film (2).mkv"
+ assert not list(root.glob("*.part")), "the part was left behind"
+
+
+def test_without_hard_links_a_file_is_still_not_replaced(tmp_path, monkeypatch):
+ """FAT, exFAT and some network shares have no hard links."""
+ from meshbay_node import roots as roots_mod
+
+ def no_links(*a, **k):
+ raise PermissionError("operation not permitted")
+ monkeypatch.setattr(roots_mod.os, "link", no_links)
+ (tmp_path / "a.txt").write_bytes(b"there first")
+ part = tmp_path / "a.txt.0123abcd.part"
+ part.write_bytes(b"uploaded")
+
+ name = roots_mod.publish_upload(part, tmp_path, "a.txt", "a.txt")
+ assert name == "a (2).txt"
+ assert (tmp_path / "a.txt").read_bytes() == b"there first"
+ assert (tmp_path / "a (2).txt").read_bytes() == b"uploaded"
+ assert not part.exists()
+
+
+# ── files Explorer acts on by itself ─────────────────────────────────────────
+
+
+@pytest.mark.parametrize("name", ["desktop.ini", "Desktop.INI", "photos.lnk", "site.url",
+ "x.scf", "Docs.library-ms", "s.searchConnector-ms"])
+async def test_a_file_explorer_acts_on_is_refused(tmp_path, name):
+ ctx = _group_ctx(tmp_path)
+ peer = _peer(ctx)
+ await peer._do_file_upload(sealed_upload(peer, filename=name, data=b"[x]"))
+ assert [m.get("code") for m in _errors(peer)] == ["file_type_refused"]
+ assert not any(ctx["roots"].roots[0].path.iterdir())
+
+
+async def test_an_ordinary_file_with_a_near_name_is_accepted(tmp_path):
+ ctx = _group_ctx(tmp_path)
+ peer = _peer(ctx)
+ await peer._do_file_upload(sealed_upload(peer, filename="url-notes.txt", data=b"x"))
+ assert _errors(peer) == []
+
+
+
+def test_a_part_the_node_did_not_write_is_never_deleted():
+ """A browser's download in progress, a copy the operator is making: a
+ `.part` without the node's tag is somebody else's, however old."""
+ doomed = orphaned_parts(
+ [(Path("/roots/media/film.mkv.part"), _old(ORPHAN_AFTER_SECS * 10)),
+ (Path("/roots/media/report.pdf.part"), _old(ORPHAN_AFTER_SECS * 10)),
+ (Path("/roots/media/film.mkv.0123abcd.part"), _old(ORPHAN_AFTER_SECS * 10))],
+ live=set(), now=NOW)
+ assert doomed == [Path("/roots/media/film.mkv.0123abcd.part")]