c627369c5de0c44d6b6d6cbb12921f9382d17276
[dups.git] / dups.ml
1 open Printf
2
3 module Array = ArrayLabels
4 module List = ListLabels
5 module StrSet = Set.Make(String)
6 module Unix = UnixLabels
7
8 module Stream : sig
9 type 'a t
10
11 val create : (unit -> 'a option) -> 'a t
12
13 val iter : 'a t -> f:('a -> unit) -> unit
14
15 val map : 'a t -> f:('a -> 'b) -> 'b t
16
17 val filter : 'a t -> f:('a -> bool) -> 'a t
18
19 val concat : ('a t) list -> 'a t
20 end = struct
21 module S = Stream
22
23 type 'a t =
24 {mutable streams : ('a S.t) list}
25
26 let create f =
27 {streams = [S.from (fun _ -> f ())]}
28
29 let rec next t =
30 match t.streams with
31 | [] ->
32 None
33 | s :: streams ->
34 (match S.next s with
35 | exception Stream.Failure ->
36 t.streams <- streams;
37 next t
38 | x ->
39 Some x
40 )
41
42 let map t ~f =
43 create (fun () ->
44 match next t with
45 | None -> None
46 | Some x -> Some (f x)
47 )
48
49 let filter t ~f =
50 let rec filter () =
51 match next t with
52 | None ->
53 None
54 | Some x when f x ->
55 Some x
56 | Some _ ->
57 filter ()
58 in
59 create filter
60
61 let iter t ~f =
62 List.iter t.streams ~f:(S.iter f)
63
64 let concat ts =
65 {streams = List.concat (List.map ts ~f:(fun {streams} -> streams))}
66 end
67
68 module In_channel : sig
69 val lines : in_channel -> string Stream.t
70 end = struct
71 let lines ic =
72 Stream.create (fun () ->
73 match input_line ic with
74 | exception End_of_file ->
75 None
76 | line ->
77 Some line
78 )
79 end
80
81 module File : sig
82 type t =
83 { path : string
84 ; size : int
85 }
86
87 val find : string -> t Stream.t
88 (** Find all files in the directory tree, starting from the given root path *)
89
90 val lookup : string Stream.t -> t Stream.t
91 (** Lookup file info for given paths *)
92
93 module Set : sig include Set.S with type elt := t end
94 end = struct
95 type t =
96 { path : string
97 ; size : int
98 }
99
100 let compare {path=p1; _} {path=p2; _} =
101 Stdlib.compare p1 p2
102
103 let lookup paths =
104 Stream.map paths ~f:(fun path ->
105 let {Unix.st_size = size; _} = Unix.lstat path in
106 {path; size}
107 )
108
109 let find root =
110 let dirs = Queue.create () in
111 let files = Queue.create () in
112 let explore parent =
113 Array.iter (Sys.readdir parent) ~f:(fun child ->
114 let path = Filename.concat parent child in
115 let {Unix.st_kind = file_kind; st_size; _} = Unix.lstat path in
116 match file_kind with
117 | Unix.S_REG ->
118 let file = {path; size = st_size} in
119 Queue.add file files
120 | Unix.S_DIR ->
121 Queue.add path dirs
122 | Unix.S_CHR
123 | Unix.S_BLK
124 | Unix.S_LNK
125 | Unix.S_FIFO
126 | Unix.S_SOCK ->
127 ()
128 )
129 in
130 explore root;
131 let rec next () =
132 match Queue.is_empty files, Queue.is_empty dirs with
133 | false, _ -> Some (Queue.take files)
134 | true , true -> None
135 | true , false ->
136 explore (Queue.take dirs);
137 next ()
138 in
139 Stream.create next
140
141 module Set = Set.Make(struct
142 type elt = t
143 type t = elt
144 let compare = compare
145 end)
146 end
147
148 type input =
149 | Stdin
150 | Directories of string list
151
152 type output =
153 | Stdout
154 | Directory of string
155
156 type opt =
157 { input : input
158 ; output : output
159 ; ignore : Str.regexp option
160 ; sample : int
161 }
162
163 type count =
164 { considered : int ref
165 ; empty : int ref
166 ; ignored : int ref
167 ; unique_size : int ref
168 ; unique_sample : int ref
169 ; hashed : int ref
170 }
171
172 let make_input_stream input ignore count =
173 let input =
174 match input with
175 | Stdin ->
176 File.lookup (In_channel.lines stdin)
177 | Directories paths ->
178 let paths = StrSet.elements (StrSet.of_list paths) in
179 Stream.concat (List.map paths ~f:File.find)
180 in
181 Stream.filter input ~f:(fun {File.path; size} ->
182 incr count.considered;
183 let empty = size = 0 in
184 let ignored =
185 match ignore with
186 | Some regexp when (Str.string_match regexp path 0) ->
187 true
188 | Some _ | None ->
189 false
190 in
191 if empty then incr count.empty;
192 if ignored then incr count.ignored;
193 (not empty) && (not ignored)
194 )
195
196 let make_output_fun = function
197 | Stdout ->
198 fun digest n_files files ->
199 printf "%s %d\n%!" (Digest.to_hex digest) n_files;
200 List.iter (File.Set.elements files) ~f:(fun {File.path; _} ->
201 printf " %S\n%!" path
202 )
203 | Directory dir ->
204 fun digest _ files ->
205 let digest = Digest.to_hex digest in
206 let dir = Filename.concat dir (String.sub digest 0 2) in
207 Unix.mkdir dir ~perm:0o700;
208 let oc = open_out (Filename.concat dir digest) in
209 List.iter (File.Set.elements files) ~f:(fun {File.path; _} ->
210 output_string oc (sprintf "%S\n%!" path)
211 );
212 close_out oc
213
214 let sample path ~len =
215 let buf = Bytes.make len ' ' in
216 let ic = open_in_bin path in
217 let rec read pos len =
218 assert (len >= 0);
219 if len = 0 then
220 ()
221 else begin
222 let chunk_size = input ic buf pos len in
223 if chunk_size = 0 then (* EOF *)
224 ()
225 else
226 read (pos + chunk_size) (len - chunk_size)
227 end
228 in
229 read 0 len;
230 close_in ic;
231 Bytes.to_string buf
232
233 let main {input; output; ignore; sample = sample_len} =
234 let t0 = Sys.time () in
235 let count =
236 { considered = ref 0
237 ; empty = ref 0
238 ; ignored = ref 0
239 ; unique_size = ref 0
240 ; hashed = ref 0
241 ; unique_sample = ref 0
242 }
243 in
244 let output = make_output_fun output in
245 let input = make_input_stream input ignore count in
246 let files_by_size = Hashtbl.create 1_000_000 in
247 let files_by_sample = Hashtbl.create 1_000_000 in
248 let files_by_digest = Hashtbl.create 1_000_000 in
249 let process tbl ~group ~file =
250 let count, files =
251 match Hashtbl.find_opt tbl group with
252 | None ->
253 (0, File.Set.empty)
254 | Some (n, files) ->
255 (n, files)
256 in
257 Hashtbl.replace tbl group (count + 1, File.Set.add file files)
258 in
259 (* TODO: Make a nice(r) abstraction to re-assemble pieces in the pipeline:
260 *
261 * from input to files_by_size
262 * from files_by_size to files_by_sample
263 * from files_by_sample to files_by_digest
264 * from files_by_digest to output
265 *
266 * input |> files_by_size |> files_by_sample |> files_by_digest |> output
267 *)
268 Stream.iter input ~f:(fun ({File.size; _} as file) ->
269 process files_by_size ~group:size ~file
270 );
271 Hashtbl.iter
272 (fun _ (n, files) ->
273 (* Skip files with unique sizes *)
274 if n > 1 then
275 File.Set.iter
276 (fun ({File.path; _} as file) ->
277 process
278 files_by_sample
279 ~group:(sample path ~len:sample_len)
280 ~file
281 )
282 files
283 else
284 incr count.unique_size;
285 )
286 files_by_size;
287 Hashtbl.iter
288 (fun _ (n, files) ->
289 (* Skip files with unique samples *)
290 if n > 1 then
291 File.Set.iter
292 (fun ({File.path; _} as file) ->
293 incr count.hashed;
294 process files_by_digest ~group:(Digest.file path) ~file
295 )
296 files
297 else
298 incr count.unique_sample;
299 )
300 files_by_sample;
301 Hashtbl.iter
302 (fun d (n, files) ->
303 if n > 1 then
304 output d n files
305 )
306 files_by_digest;
307 let t1 = Sys.time () in
308 eprintf "Time : %f seconds\n%!" (t1 -. t0);
309 eprintf "Considered : %d\n%!" !(count.considered);
310 eprintf "Hashed : %d\n%!" !(count.hashed);
311 eprintf "Skipped due to 0 size : %d\n%!" !(count.empty);
312 eprintf "Skipped due to unique size : %d\n%!" !(count.unique_size);
313 eprintf "Skipped due to unique sample : %d\n%!" !(count.unique_sample);
314 eprintf "Ignored due to regex match : %d\n%!" !(count.ignored)
315
316 let get_opt () : opt =
317 let assert_ test x msg =
318 if not (test x) then begin
319 eprintf "%s\n%!" msg;
320 exit 1
321 end
322 in
323 let assert_file_exists path =
324 assert_ Sys.file_exists path (sprintf "File does not exist: %S" path)
325 in
326 let assert_file_is_dir path =
327 assert_ Sys.is_directory path (sprintf "File is not a directory: %S" path)
328 in
329 let input = ref Stdin in
330 let output = ref Stdout in
331 let ignore = ref None in
332 let sample = ref 256 in
333 let spec =
334 [ ( "-out"
335 , Arg.String (fun path ->
336 assert_file_exists path;
337 assert_file_is_dir path;
338 output := Directory path
339 )
340 , " Output to this directory instead of stdout."
341 )
342 ; ( "-ignore"
343 , Arg.String (fun regexp -> ignore := Some (Str.regexp regexp))
344 , " Ignore file paths which match this regexp pattern (see Str module)."
345 )
346 ; ( "-sample"
347 , Arg.Set_int sample
348 , (sprintf " Byte size of file samples to use. Default: %d" !sample)
349 )
350 ]
351 in
352 Arg.parse
353 (Arg.align spec)
354 (fun path ->
355 assert_file_exists path;
356 assert_file_is_dir path;
357 match !input with
358 | Stdin ->
359 input := Directories [path]
360 | Directories paths ->
361 input := Directories (path :: paths)
362 )
363 "";
364 assert_
365 (fun x -> x > 0)
366 !sample
367 (sprintf "Sample size cannot be negative: %d" !sample);
368 { input = !input
369 ; output = !output
370 ; ignore = !ignore
371 ; sample = !sample
372 }
373
374 let () =
375 main (get_opt ())
This page took 0.062195 seconds and 3 git commands to generate.