@@ -25,8 +25,10 @@ defmodule Vutuv.Images do
2525 a mirror, written by `mirror/2` and dropped by `forget/2`, joined on the
2626 `token` both carry rather than by a pointer. Nothing here reads it yet, which
2727 is why `freeze/1` raises for one and `takedown_ready?/1` answers false: a case
28- opened on a row nobody consults would take nothing offline. Moving the readers
29- across, and only then retiring the old table, is the deploy after.
28+ opened on a row nobody consults would take nothing offline. **Such a kind
29+ moves in three releases**: the mirror, then the one that moves the readers and
30+ wires the takedown, then the migration that retires the old table.
31+ `docs/architecture/images.md` spells the three out.
3032
3133 `serving/1` is the second thing here that is not a column: how a kind reaches
3234 a reader decides what its off switch is, and a kind nobody has declared
@@ -66,7 +68,9 @@ defmodule Vutuv.Images do
6668 # compares the two and fails the build on it.
6769 #
6870 # An entry goes when that kind's contract release retires its old table,
69- # together with the double write; `takedown_ready?/1` below flips with it.
71+ # together with the double write. It does **not** take the report gate with
72+ # it: that reads `@takedown` below, which the release before that one, the one
73+ # that moves the readers and wires the takedown, is what extends.
7074 # #2052 (post photos) and #2053 (organization images) add one entry each;
7175 # #2055 (review covers) does **not** — a review's cover is columns on the
7276 # review row with no token and no table, which is the `@profile_columns`
@@ -649,33 +653,44 @@ defmodule Vutuv.Images do
649653 :ok
650654 end
651655
656+ # Which takedown a kind gets, and (by the presence of a key) whether it has
657+ # one at all. `takedown_ready?/1` and the three functions below all guard on
658+ # `is_map_key(@takedown, kind)`, so the gate that lets a report name a picture
659+ # and the code that takes it offline read one map and cannot answer
660+ # differently. A kind arrives here in the same change as its strategy's
661+ # clauses (issue #2057).
662+ #
663+ # The gate used to be derived from `@mirrored` instead, and the two agreed by
664+ # arithmetic rather than by meaning: that entry is what a **contract** release
665+ # deletes, so the gate would have opened on the deploy that retires a kind's
666+ # old table whether or not anything had wired that kind's freeze, and the
667+ # first report accepted on it would have raised in front of the admin who
668+ # upheld it.
669+ @ takedown Map . new ( @ profile_kinds , & { & 1 , :profile } )
670+
652671 @ doc """
653672 Whether a copyright case can act on this picture at all — what
654673 `Vutuv.Moderation` asks before letting a report name it.
655674
656- A gallery kind that has only just arrived here (#2054 and its siblings ship
657- the **expand** half: the row exists, every URL and every gate still reads the
658- old table) has no takedown path yet, and `freeze/1` below has no clause for
659- it. Answering "yes" would open a case whose uphold raises, so a report is
660- refused until the kind's own contract release wires the freeze — which is all
661- there was before the row existed.
662-
663- Derived from `mirrored?/1` rather than listed a second time, so the two
664- cannot drift: "this picture's truth is still elsewhere" and "nothing here can
665- take it offline" are one fact, and the contract release that deletes the
666- mirror entry flips both at once.
675+ True for exactly the kinds `freeze/1` below can act on (`@takedown`), because
676+ that is the fact the gate needs: a report accepted on a kind nothing can take
677+ offline opens a case whose uphold raises. A gallery kind that has only just
678+ arrived here (#2054 and its siblings ship the **expand** half: the row exists,
679+ every URL and every gate still reads the old table) is therefore refused until
680+ the release that wires its takedown, which leaves it the affordance it had
681+ before the row existed: reporting the posting or the page the picture sits on.
667682 """
668- def takedown_ready? ( % Image { kind: kind } ) , do: kind in @ kinds and not mirrored? ( kind )
683+ def takedown_ready? ( % Image { kind: kind } ) , do: is_map_key ( @ takedown , kind )
669684
670- # A kind whose row exists but whose freeze does not. Loud rather than
685+ # A kind whose row exists but whose takedown does not. Loud rather than
671686 # half-done: the alternative is a stamped `frozen_at` no reader consults and
672687 # files nothing moved, which reads from the case page exactly like a
673688 # completed takedown.
674689 defp no_takedown_path! ( % Image { kind: kind } , action ) do
675690 raise ArgumentError , """
676- cannot #{ action } an image of kind #{ inspect ( kind ) } : this release mirrors it \
677- into the images table but no reader consults the row yet, so nothing would \
678- go offline. Wire the kind's freeze before letting a case reach it.\
691+ cannot #{ action } an image of kind #{ inspect ( kind ) } : nothing here can take a \
692+ picture of that kind offline, so nothing would go offline. Give the kind a \
693+ strategy in Vutuv.Images' @takedown before letting a case reach it.\
679694 """
680695 end
681696
@@ -703,7 +718,14 @@ defmodule Vutuv.Images do
703718 already invisible and a job `reconcile_holds/0` finishes. The other order
704719 would leave files in a hold that nothing knows to bring back.
705720 """
706- def freeze ( % Image { kind: kind } = image ) when kind in @ profile_kinds do
721+ def freeze ( % Image { kind: kind } = image ) when is_map_key ( @ takedown , kind ) ,
722+ do: freeze_by ( @ takedown [ kind ] , image )
723+
724+ def freeze ( % Image { } = image ) , do: no_takedown_path! ( image , "freeze" )
725+
726+ # The profile strategy: the files are served straight off disk (`serving/1`
727+ # answers `:static`), so taking the picture offline means moving them.
728+ defp freeze_by ( :profile , % Image { } = image ) do
707729 # `is_nil(frozen_at)` so a second pass — `reconcile_holds/0` finishing an
708730 # interrupted move — re-asserts the freeze without moving the moment it
709731 # happened, which is what the case and the statement of reasons quote.
@@ -720,8 +742,6 @@ defmodule Vutuv.Images do
720742 :ok
721743 end
722744
723- def freeze ( % Image { } = image ) , do: no_takedown_path! ( image , "freeze" )
724-
725745 @ doc """
726746 Puts a frozen picture back exactly where it was: every file returns to the
727747 tree it came from under the name it had, the member row gets its four columns
@@ -738,7 +758,12 @@ defmodule Vutuv.Images do
738758 only once the member row names the files again, so a half-finished restore is
739759 still a hold for `reconcile_holds/0` to find.
740760 """
741- def unfreeze ( % Image { kind: kind } = image ) when kind in @ profile_kinds do
761+ def unfreeze ( % Image { kind: kind } = image ) when is_map_key ( @ takedown , kind ) ,
762+ do: unfreeze_by ( @ takedown [ kind ] , image )
763+
764+ def unfreeze ( % Image { } = image ) , do: no_takedown_path! ( image , "unfreeze" )
765+
766+ defp unfreeze_by ( :profile , % Image { } = image ) do
742767 { _count , _ } =
743768 Repo . update_all ( from ( i in Image , where: i . id == ^ image . id ) ,
744769 set: [ frozen_at: nil , updated_at: now ( ) ]
@@ -756,8 +781,6 @@ defmodule Vutuv.Images do
756781 :ok
757782 end
758783
759- def unfreeze ( % Image { } = image ) , do: no_takedown_path! ( image , "unfreeze" )
760-
761784 @ doc """
762785 Deletes this picture for good — every derived version, the private original
763786 and the held copies — and forgets the row. What an upheld copyright case does,
@@ -767,7 +790,12 @@ defmodule Vutuv.Images do
767790 nothing points at (which `reconcile_holds/0` collects), never a member row
768791 naming files that are gone.
769792 """
770- def purge ( % Image { kind: kind } = image ) when kind in @ profile_kinds do
793+ def purge ( % Image { kind: kind } = image ) when is_map_key ( @ takedown , kind ) ,
794+ do: purge_by ( @ takedown [ kind ] , image )
795+
796+ def purge ( % Image { } = image ) , do: no_takedown_path! ( image , "purge" )
797+
798+ defp purge_by ( :profile , % Image { } = image ) do
771799 case owner ( image ) do
772800 % User { } = user ->
773801 config = @ profile_columns [ image . kind ]
@@ -785,8 +813,6 @@ defmodule Vutuv.Images do
785813 :ok
786814 end
787815
788- def purge ( % Image { } = image ) , do: no_takedown_path! ( image , "purge" )
789-
790816 @ doc """
791817 Finishes every move a dying slot left half-done, in both directions — the
792818 standing job behind `freeze/1` and `unfreeze/1`, run by
0 commit comments