Skip to content

Commit 83d86ec

Browse files
authored
Merge pull request #30 from nationalarchives/29-handle-existing-annotation-id
29 handle existing annotation
2 parents bcc65cd + b00e3ec commit 83d86ec

9 files changed

Lines changed: 165 additions & 44 deletions

File tree

miiify/bin/clone.ml

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ let dir_is_nonempty path =
1313
(* treat any entry (including hidden) as non-empty; this is a safety check *)
1414
List.length items > 0
1515

16-
let run_clone ~repo_url ~git_path ~force =
16+
let run_clone ~repo_url ~git_path =
1717
let* () = Lwt_io.printl "Miiify Clone" in
1818
let* () = Lwt_io.printlf "Remote: %s" repo_url in
1919
let* () = Lwt_io.printlf "Local: %s" git_path in
2020
let* () = Lwt_io.printl "" in
2121

22-
(* Safer default: refuse to clone into an existing non-empty directory unless forced. *)
22+
(* Refuse to clone into an existing non-empty directory. *)
2323
let* () =
2424
if Sys.file_exists git_path then (
2525
if not (Sys.is_directory git_path) then (
@@ -29,21 +29,18 @@ let run_clone ~repo_url ~git_path ~force =
2929
git_path
3030
in
3131
Lwt.fail (Failure "Invalid --git path")
32-
) else if dir_is_nonempty git_path && not force then (
32+
) else if dir_is_nonempty git_path then (
3333
let* () =
3434
Lwt_io.eprintlf
3535
"Error: --git directory already exists and is not empty: %s"
3636
git_path
3737
in
3838
let* () =
3939
Lwt_io.eprintl
40-
"Refusing to clone into an existing store. Use --force to reuse it."
40+
"Remove it first if you want to start fresh: rm -rf <git-path>"
4141
in
4242
Lwt.fail (Failure "Refusing to clone into non-empty directory")
43-
) else if dir_is_nonempty git_path && force then
44-
Lwt_io.eprintl
45-
"Warning: --force set; reusing existing --git directory (HEAD may change)"
46-
else
43+
) else
4744
Lwt.return_unit
4845
) else
4946
Lwt.return_unit
@@ -126,12 +123,12 @@ let run_clone ~repo_url ~git_path ~force =
126123
Lwt.fail (Failure "Clone failed")
127124
))
128125

129-
let clone_repo repo_url git_path force =
126+
let clone_repo repo_url git_path =
130127
let result =
131128
Lwt_main.run
132129
(Lwt.catch
133130
(fun () ->
134-
let* () = run_clone ~repo_url ~git_path ~force in
131+
let* () = run_clone ~repo_url ~git_path in
135132
Lwt.return (Ok ()))
136133
(fun exn -> Lwt.return (Error exn)))
137134
in
@@ -152,23 +149,19 @@ let git_path =
152149
let doc = "Local Irmin Git store directory" in
153150
Arg.(value & opt string "git_store" & info ["git"; "g"] ~docv:"DIR" ~doc)
154151

155-
let force_flag =
156-
let doc = "Allow cloning into an existing non-empty --git directory (reuse store; may move HEAD)" in
157-
Arg.(value & flag & info ["force"; "f"] ~doc)
158-
159152
let cmd =
160153
let doc = "Clone remote Git repository into Irmin Git store" in
161154
let man = [
162155
`S Manpage.s_description;
163156
`P "Clones a remote Git repository into a local Irmin Git store.";
164157
`P "Fetches from remote using Irmin's native sync mechanism.";
165-
`P "By default, refuses to clone into an existing non-empty --git directory.";
158+
`P "Refuses to clone into an existing non-empty --git directory. Remove it first if needed.";
166159
`P "Example:";
167160
`Pre " miiify-clone https://github.com/org/annotations.git";
168161
`Pre " miiify-clone https://github.com/org/annotations.git --git ./my-db";
169162
] in
170163
let info = Cmd.info "clone" ~version:"0.1.0" ~doc ~man in
171-
Cmd.v info Term.(const clone_repo $ repo_url $ git_path $ force_flag)
164+
Cmd.v info Term.(const clone_repo $ repo_url $ git_path)
172165

173166
let () =
174167
(* Initialize RNG for git-paf/mirage-crypto *)

miiify/bin/compile.ml

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,17 +65,25 @@ let process_annotation git_path data validate =
6565
Lwt.fail (Failure ("JSON validation failed for " ^ String.concat "/" git_path))
6666
in
6767

68-
(* Check if user supplied an ID - warn that it will be ignored *)
68+
(* Check for existing id - error if --validate, warn and continue otherwise *)
6969
let* () =
7070
try
7171
let json = Yojson.Basic.from_string data in
7272
match Yojson.Basic.Util.member "id" json with
7373
| `Null -> Lwt.return_unit
7474
| `String supplied_id ->
75-
Lwt_io.printlf "ℹ %s - Ignoring supplied ID (%s)"
76-
(String.concat "/" git_path) supplied_id
75+
if validate then
76+
let* () =
77+
Lwt_io.eprintlf "✗ %s - annotation must not contain an id (%s)"
78+
(String.concat "/" git_path) supplied_id
79+
in
80+
Lwt.fail (Failure ("Annotation contains existing id: " ^ String.concat "/" git_path))
81+
else
82+
Lwt_io.printlf "ℹ %s - Ignoring supplied ID (%s)"
83+
(String.concat "/" git_path) supplied_id
7784
| _ -> Lwt.return_unit
78-
with _ -> Lwt.return_unit
85+
with Failure _ as e -> Lwt.fail e
86+
| _ -> Lwt.return_unit
7987
in
8088

8189
(* Validate against schema if flag is set *)

miiify/bin/import.ml

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,17 +66,25 @@ let import_annotation container_id path validate =
6666
Lwt.return_unit
6767
in
6868

69-
(* Check if user supplied an ID - warn that it will be ignored *)
69+
(* Check for existing id - error if --validate, warn and continue otherwise *)
7070
let* () =
7171
try
7272
let json = Yojson.Basic.from_string content in
7373
match Yojson.Basic.Util.member "id" json with
7474
| `Null -> Lwt.return_unit
7575
| `String supplied_id ->
76-
Lwt_io.printlf "ℹ %s/%s - Ignoring supplied ID (%s)"
77-
container_id filename supplied_id
76+
if validate then
77+
let* () =
78+
Lwt_io.eprintlf "✗ %s/%s - annotation must not contain an id (%s)"
79+
container_id filename supplied_id
80+
in
81+
Lwt.fail (Failure ("Annotation contains existing id in " ^ path))
82+
else
83+
Lwt_io.printlf "ℹ %s/%s - Ignoring supplied ID (%s)"
84+
container_id filename supplied_id
7885
| _ -> Lwt.return_unit
79-
with _ -> Lwt.return_unit
86+
with Failure _ as e -> Lwt.fail e
87+
| _ -> Lwt.return_unit
8088
in
8189

8290
(* Return key and data for batch commit *)
@@ -283,6 +291,20 @@ let import_directory input_dir git_path validate =
283291
Printf.eprintf "Error: Path is not a directory: %s\n" input_dir;
284292
exit 1
285293
);
294+
295+
(* Refuse to import into an existing non-empty git store unless forced *)
296+
if Sys.file_exists git_path then (
297+
if not (Sys.is_directory git_path) then (
298+
Printf.eprintf "Error: --git path exists and is not a directory: %s\n" git_path;
299+
exit 1
300+
) else
301+
let entries = Sys.readdir git_path |> Array.to_list in
302+
if List.length entries > 0 then (
303+
Printf.eprintf "Error: --git directory already exists and is not empty: %s\n" git_path;
304+
Printf.eprintf "Remove it first if you want to start fresh: rm -rf <git-path>\n";
305+
exit 1
306+
)
307+
);
286308

287309
let result =
288310
Lwt_main.run
@@ -320,6 +342,7 @@ let cmd =
320342
`S Manpage.s_description;
321343
`P "Imports JSON annotation files into Irmin Git store for development/testing.";
322344
`P "In production, use 'miiify clone' to clone a remote repository instead.";
345+
`P "Refuses to import into an existing non-empty --git directory. Remove it first if needed.";
323346
`P "Example:";
324347
`Pre " miiify-import --input ./annotations --git ./git_store";
325348
`Pre " miiify-import --input ./annotations --git ./git_store --validate";

miiify/dune-project

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
(name miiify)
44

5-
(version 2.0.1)
5+
(version 2.0.2)
66

77
(generate_opam_files true)
88

miiify/lib/annotation.ml

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -69,20 +69,13 @@ let page json ~page ~total ~limit =
6969

7070
let annotation json = json |> to_string
7171

72-
(* Inject ID into annotation for read-only serving *)
72+
(* Inject ID into annotation for read-only serving.
73+
Any existing id field is replaced with the server-generated IRI. *)
7374
let inject_id json ~container_id ~annotation_id ~base_url =
74-
let open Util in
75-
let existing_id = json |> member "id" in
76-
match existing_id with
77-
| `Null ->
78-
(* No ID present, inject it *)
79-
let iri = base_url ^ "/" ^ container_id ^ "/" ^ annotation_id in
80-
combine (`Assoc [ ("id", `String iri) ]) json
81-
| `String id ->
82-
(* ID exists but might not match server URL, update it *)
83-
let iri = base_url ^ "/" ^ container_id ^ "/" ^ annotation_id in
84-
if id <> iri then
85-
combine (`Assoc [ ("id", `String iri) ]) json
86-
else
87-
json
88-
| _ -> json
75+
let iri = base_url ^ "/" ^ container_id ^ "/" ^ annotation_id in
76+
let stripped =
77+
match json with
78+
| `Assoc pairs -> `Assoc (List.filter (fun (k, _) -> k <> "id") pairs)
79+
| other -> other
80+
in
81+
Util.combine (`Assoc [ ("id", `String iri) ]) stripped

miiify/miiify.opam

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This file is generated by dune, edit dune-project instead
22
opam-version: "2.0"
3-
version: "2.0.1"
3+
version: "2.0.2"
44
synopsis: "A web annotation server"
55
description:
66
"A simple web annotation server built from the same principles at Git"

miiify/test/response_test.ml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,20 @@ let test_id_replacement _switch () =
8686
Miiify.Controller.get_annotation ~db ~container_id ~annotation_id ~base_url
8787
in
8888
let json = Yojson.Basic.from_string result in
89-
let id = Yojson.Basic.Util.member "id" json |> Yojson.Basic.Util.to_string in
9089

90+
(* Confirm the id is the server-generated IRI *)
91+
let id = Yojson.Basic.Util.member "id" json |> Yojson.Basic.Util.to_string in
9192
Alcotest.(check string) "annotation has server-generated ID (not supplied ID)"
9293
"http://localhost:10000/my-canvas/highlight-1" id;
94+
95+
(* Confirm no duplicate id keys (JSON parsed as assoc should have exactly one "id") *)
96+
let id_count =
97+
match json with
98+
| `Assoc pairs -> List.length (List.filter (fun (k, _) -> k = "id") pairs)
99+
| _ -> 0
100+
in
101+
Alcotest.(check int) "exactly one id key in output" 1 id_count;
102+
93103
Lwt.return_unit
94104

95105
(* Test: ID injection into container *)

miiify/test/smoke_test.ml

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,72 @@ let test_import_compile_then_http _switch () =
6666

6767
Lwt.return_unit
6868

69+
(* Test: annotation with existing id is warned but accepted without --validate *)
70+
let test_import_with_id_no_validate _switch () =
71+
let ws = make_temp_workspace "id-no-validate" in
72+
let _ =
73+
write_annotation_file ~annotations_dir:ws.annotations_dir
74+
~container_id:"my-canvas" ~slug:"anno-with-id" ~contents:annotation_with_id
75+
in
76+
let result =
77+
run_miiify_import ~annotations_dir:ws.annotations_dir ~git_repo:ws.git_repo
78+
in
79+
Alcotest.(check int) "import succeeds without --validate when annotation has id" 0 result;
80+
Lwt.return_unit
81+
82+
(* Test: annotation with existing id fails with --validate *)
83+
let test_import_with_id_validate _switch () =
84+
let ws = make_temp_workspace "id-validate" in
85+
let _ =
86+
write_annotation_file ~annotations_dir:ws.annotations_dir
87+
~container_id:"my-canvas" ~slug:"anno-with-id" ~contents:annotation_with_id
88+
in
89+
let result =
90+
run_miiify_import_validate ~annotations_dir:ws.annotations_dir ~git_repo:ws.git_repo
91+
in
92+
Alcotest.(check bool) "import fails with --validate when annotation has id" true (result <> 0);
93+
Lwt.return_unit
94+
95+
(* Test: compile with existing id is warned but accepted without --validate *)
96+
let test_compile_with_id_no_validate _switch () =
97+
let ws = make_temp_workspace "compile-id-no-validate" in
98+
let _ =
99+
write_annotation_file ~annotations_dir:ws.annotations_dir
100+
~container_id:"my-canvas" ~slug:"anno-with-id" ~contents:annotation_with_id
101+
in
102+
let import_result =
103+
run_miiify_import ~annotations_dir:ws.annotations_dir ~git_repo:ws.git_repo
104+
in
105+
Alcotest.(check int) "import succeeds" 0 import_result;
106+
let result = run_miiify_compile ~git_repo:ws.git_repo ~pack_repo:ws.pack_repo in
107+
Alcotest.(check int) "compile succeeds without --validate when annotation has id" 0 result;
108+
Lwt.return_unit
109+
110+
(* Test: compile with existing id fails with --validate *)
111+
let test_compile_with_id_validate _switch () =
112+
let ws = make_temp_workspace "compile-id-validate" in
113+
let _ =
114+
write_annotation_file ~annotations_dir:ws.annotations_dir
115+
~container_id:"my-canvas" ~slug:"anno-with-id" ~contents:annotation_with_id
116+
in
117+
let import_result =
118+
run_miiify_import ~annotations_dir:ws.annotations_dir ~git_repo:ws.git_repo
119+
in
120+
Alcotest.(check int) "import succeeds" 0 import_result;
121+
let result = run_miiify_compile_validate ~git_repo:ws.git_repo ~pack_repo:ws.pack_repo in
122+
Alcotest.(check bool) "compile fails with --validate when annotation has id" true (result <> 0);
123+
Lwt.return_unit
124+
69125
let () =
70126
Lwt_main.run @@
71127
run "Miiify Smoke Tests"
72-
[ ("E2E", [ test_case "import+compile+http" `Quick test_import_compile_then_http ]) ]
128+
[
129+
("E2E", [ test_case "import+compile+http" `Quick test_import_compile_then_http ]);
130+
( "ID handling",
131+
[
132+
test_case "import: id ignored without --validate" `Quick test_import_with_id_no_validate;
133+
test_case "import: id rejected with --validate" `Quick test_import_with_id_validate;
134+
test_case "compile: id ignored without --validate" `Quick test_compile_with_id_no_validate;
135+
test_case "compile: id rejected with --validate" `Quick test_compile_with_id_validate;
136+
] );
137+
]

miiify/test/test_support.ml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,19 @@ let comment_annotation =
6262

6363
let comment_target = "https://example.com/iiif/canvas/1#xywh=300,150,100,75"
6464

65+
let annotation_with_id =
66+
{|{
67+
"id": "https://example.com/existing-id",
68+
"type": "Annotation",
69+
"motivation": "highlighting",
70+
"body": {
71+
"type": "TextualBody",
72+
"value": "Has an existing id",
73+
"purpose": "commenting"
74+
},
75+
"target": "https://example.com/iiif/canvas/1#xywh=100,100,200,50"
76+
}|}
77+
6578
let write_file path contents =
6679
let oc = open_out path in
6780
output_string oc contents;
@@ -120,6 +133,14 @@ let run_miiify_import ~annotations_dir ~git_repo =
120133
in
121134
Sys.command cmd
122135

136+
let run_miiify_import_validate ~annotations_dir ~git_repo =
137+
let cmd =
138+
Printf.sprintf "%s --input %s --git %s --validate > /dev/null 2>&1"
139+
(Filename.quote (import_exe ())) (Filename.quote annotations_dir)
140+
(Filename.quote git_repo)
141+
in
142+
Sys.command cmd
143+
123144
let run_miiify_compile ~git_repo ~pack_repo =
124145
let cmd =
125146
Printf.sprintf "%s --git %s --pack %s > /dev/null 2>&1"
@@ -128,6 +149,14 @@ let run_miiify_compile ~git_repo ~pack_repo =
128149
in
129150
Sys.command cmd
130151

152+
let run_miiify_compile_validate ~git_repo ~pack_repo =
153+
let cmd =
154+
Printf.sprintf "%s --git %s --pack %s --validate > /dev/null 2>&1"
155+
(Filename.quote (compile_exe ())) (Filename.quote git_repo)
156+
(Filename.quote pack_repo)
157+
in
158+
Sys.command cmd
159+
131160
let create_test_db_from_files test_name =
132161
let ws = make_temp_workspace test_name in
133162

0 commit comments

Comments
 (0)