36
37:- module(web_storage,
38 [ storage_file/1, 39 storage_file_extension/2, 40 storage_file_extension_head/3, 41 storage_file/3, 42 storage_meta_data/2, 43 storage_meta_property/2, 44 storage_commit/2, 45
46 storage_fsck/0,
47 storage_repack/0,
48 storage_repack/1, 49 storage_unpack/0,
50
51 storage_store_term/2, 52 storage_load_term/2, 53
54 use_gitty_file/1, 55 use_gitty_file/2 56 ]). 57:- use_module(library(http/http_dispatch)). 58:- use_module(library(http/http_parameters)). 59:- use_module(library(http/http_json)). 60:- use_module(library(http/http_cors)). 61:- use_module(library(http/mimetype)). 62:- use_module(library(lists)). 63:- use_module(library(settings)). 64:- use_module(library(random)). 65:- use_module(library(apply)). 66:- use_module(library(option)). 67:- use_module(library(debug)). 68:- use_module(library(broadcast)). 69:- use_module(library(readutil)). 70:- use_module(library(solution_sequences)). 71:- use_module(library(dcg/basics)). 72:- use_module(library(pcre)). 73:- use_module(library(pengines_io)). 74
75:- use_module(page). 76:- use_module(gitty). 77:- use_module(patch). 78:- use_module(config). 79:- use_module(search). 80:- use_module(authenticate). 81:- use_module(pep). 82
83:- meta_predicate
84 use_gitty_file(:),
85 use_gitty_file(:, +). 86
94
95:- setting(directory, callable, data(storage),
96 'The directory for storing files.'). 97
98:- http_handler(swish('p/'),
99 web_storage,
100 [ id(web_storage), prefix ]). 101:- http_handler(swish('source_list'),
102 source_list,
103 [ id(source_list) ]). 104:- http_handler(swish('source_modified'),
105 source_modified,
106 [ id(source_modified) ]). 107
108:- listen(http(pre_server_start),
109 open_gittystore(_)). 110
111:- dynamic storage_dir/1. 112:- volatile storage_dir/1. 113
114open_gittystore(Dir0) :-
115 storage_dir(Dir),
116 !,
117 Dir = Dir0.
118open_gittystore(Dir) :-
119 with_mutex(web_storage, open_gittystore_guarded(Dir0)),
120 Dir = Dir0.
121
122open_gittystore_guarded(Dir) :-
123 storage_dir(Dir),
124 !.
125open_gittystore_guarded(Dir) :-
126 setting(directory, Spec),
127 absolute_file_name(Spec, Dir,
128 [ file_type(directory),
129 access(write),
130 file_errors(fail)
131 ]),
132 !,
133 gitty_open_options(Options),
134 gitty_open(Dir, Options),
135 asserta(storage_dir(Dir)).
136open_gittystore_guarded(Dir) :-
137 setting(directory, Spec),
138 absolute_file_name(Spec, Dir,
139 [ solutions(all)
140 ]),
141 \+ exists_directory(Dir),
142 create_store(Dir),
143 !,
144 gitty_open_options(Options),
145 gitty_open(Dir, Options),
146 asserta(storage_dir(Dir)).
147
148create_store(Dir) :-
149 exists_directory('storage/ref'),
150 !,
151 print_message(informational, moved_old_store(storage, Dir)),
152 rename_file(storage, Dir).
153create_store(Dir) :-
154 catch(make_directory(Dir),
155 error(permission_error(create, directory, Dir), _),
156 fail),
157 !.
158
159gitty_open_options(Options) :-
160 swish_config(redis, DB),
161 !,
162 ( swish_config(redis_prefix, Prefix)
163 -> Options = [ redis(DB),
164 redis_prefix(Prefix)
165 ]
166 ; Options = [ redis(DB)
167 ]
168 ).
169gitty_open_options([]).
170
171
178
179web_storage(Request) :-
180 memberchk(method(options), Request),
181 !,
182 cors_enable(Request,
183 [ methods([get,post,put,delete])
184 ]),
185 format('~n').
186web_storage(Request) :-
187 cors_enable(Request,
188 [ methods([get,post,put,delete])
189 ]),
190 authenticate(Request, Auth),
191 option(method(Method), Request),
192 open_gittystore(_),
193 storage(Method, Request, [identity(Auth)]).
194
195:- multifile
196 swish_config:authenticate/2,
197 swish_config:chat_count_about/2,
198 swish_config:user_profile/2. 199
200storage(get, Request, Options) :-
201 http_parameters(Request,
202 [ format(Fmt, [ oneof([swish,raw,json,history,diff]),
203 default(swish),
204 description('How to render')
205 ]),
206 depth(Depth, [ default(5),
207 integer,
208 description('History depth')
209 ]),
210 to(RelTo, [ optional(true),
211 description('Diff relative to')
212 ])
213 ]),
214 ( Fmt == history
215 -> ( nonvar(RelTo)
216 -> Format = history(Depth, RelTo)
217 ; Format = history(Depth)
218 )
219 ; Fmt == diff
220 -> Format = diff(RelTo)
221 ; Format = Fmt
222 ),
223 storage_get(Request, Format, Options).
224
225storage(post, Request, Options) :-
226 http_read_json_dict(Request, Dict),
227 option(data(Data), Dict, ""),
228 option(type(Type), Dict, pl),
229 storage_dir(Dir),
230 meta_data(Dir, Dict, _, Meta, Options),
231 ( atom_string(Base, Dict.get(meta).get(name))
232 -> file_name_extension(Base, Type, File),
233 ( authorized(gitty(create(File,named,Meta)), Options),
234 catch(gitty_create(Dir, File, Data, Meta, Commit),
235 error(gitty(file_exists(File)),_),
236 fail)
237 -> true
238 ; Error = json{error:file_exists,
239 file:File}
240 )
241 ; ( repeat,
242 random_filename(Base),
243 file_name_extension(Base, Type, File),
244 authorized(gitty(create(File,random,Meta)), Options),
245 catch(gitty_create(Dir, File, Data, Meta, Commit),
246 error(gitty(file_exists(File)),_),
247 fail)
248 -> true
249 )
250 ),
251 ( var(Error)
252 -> debug(storage, 'Created: ~p', [Commit]),
253 storage_url(File, URL),
254
255 broadcast(swish(created(File, Commit))),
256 follow(Commit, Dict),
257 reply_json_dict(json{url:URL,
258 file:File,
259 meta:Commit.put(symbolic, "HEAD")
260 })
261 ; reply_json_dict(Error)
262 ).
263storage(put, Request, Options) :-
264 http_read_json_dict(Request, Dict),
265 storage_dir(Dir),
266 request_file(Request, Dir, File),
267 ( Dict.get(update) == "meta-data"
268 -> gitty_data(Dir, File, Data, _OldMeta)
269 ; writeable(File)
270 -> option(data(Data), Dict, "")
271 ; option(path(Path), Request),
272 throw(http_reply(forbidden(Path)))
273 ),
274 meta_data(Dir, Dict, PrevMeta, Meta, Options),
275 storage_url(File, URL),
276 authorized(gitty(update(File,PrevMeta,Meta)), Options),
277 catch(gitty_update(Dir, File, Data, Meta, Commit),
278 Error,
279 true),
280 ( var(Error)
281 -> debug(storage, 'Updated: ~p', [Commit]),
282 collect_messages_as_json(
283 broadcast(swish(updated(File, Commit))),
284 Messages),
285 debug(gitty(load), 'Messages: ~p', [Messages]),
286 follow(Commit, Dict),
287 reply_json_dict(json{ url:URL,
288 file:File,
289 meta:Commit.put(symbolic, "HEAD"),
290 messages:Messages
291 })
292 ; update_error(Error, Dir, Data, File, URL)
293 ).
294storage(delete, Request, Options) :-
295 storage_dir(Dir),
296 meta_data(Dir, _{}, PrevMeta, Meta, Options),
297 request_file(Request, Dir, File),
298 authorized(gitty(delete(File,PrevMeta)), Options),
299 gitty_update(Dir, File, "", Meta, Commit),
300 broadcast(swish(deleted(File, Commit))),
301 reply_json_dict(true).
302
303writeable(File) :-
304 \+ file_name_extension(_, lnk, File).
305
310
311update_error(error(gitty(commit_version(_, Head, Previous)), _),
312 Dir, Data, File, URL) :-
313 !,
314 gitty_diff(Dir, Previous, Head, OtherEdit),
315 gitty_diff(Dir, Previous, data(Data), MyEdits),
316 Status0 = json{url:URL,
317 file:File,
318 error:edit_conflict,
319 edit:_{server:OtherEdit,
320 me:MyEdits}
321 },
322 ( OtherDiff = OtherEdit.get(data)
323 -> PatchOptions = [status(_), stderr(_)],
324 patch(Data, OtherDiff, Merged, PatchOptions),
325 Status1 = Status0.put(merged, Merged),
326 foldl(patch_status, PatchOptions, Status1, Status)
327 ; Status = Status0
328 ),
329 reply_json_dict(Status, [ status(409) ]).
330update_error(Error, _Dir, _Data, _File, _URL) :-
331 throw(Error).
332
333patch_status(status(exit(0)), Dict, Dict) :- !.
334patch_status(status(exit(Status)), Dict, Dict.put(patch_status, Status)) :- !.
335patch_status(status(killed(Signal)), Dict, Dict.put(patch_killed, Signal)) :- !.
336patch_status(stderr(""), Dict, Dict) :- !.
337patch_status(stderr(Errors), Dict, Dict.put(patch_errors, Errors)) :- !.
338
343
344follow(Commit, Dict) :-
345 Dict.get(meta).get(follow) == true,
346 _{name:File, profile_id:ProfileID} :< Commit,
347 !,
348 atom_concat('gitty:', File, DocID),
349 broadcast(swish(follow(DocID, ProfileID, [update,chat]))).
350follow(_, _).
351
357
358request_file(Request, Dir, File) :-
359 option(path_info(File), Request),
360 ( gitty_file(Dir, File, _Hash)
361 -> true
362 ; http_404([], Request)
363 ).
364
365storage_url(File, HREF) :-
366 http_link_to_id(web_storage, path_postfix(File), HREF).
367
378
379meta_data(Dict, Meta, Options) :-
380 option(identity(Auth), Options),
381 ( _ = Auth.get(identity)
382 -> HasIdentity = true
383 ; HasIdentity = false
384 ),
385 filter_auth(Auth, Auth1),
386 ( filter_meta(Dict.get(meta), HasIdentity, Meta1)
387 -> Meta = meta{}.put(Auth1).put(Meta1)
388 ; Meta = meta{}.put(Auth1)
389 ).
390
391meta_data(Store, Dict, PrevMeta, Meta, Options) :-
392 meta_data(Dict, Meta1, Options),
393 ( atom_string(Previous, Dict.get(previous)),
394 is_gitty_hash(Previous),
395 gitty_commit(Store, Previous, PrevMeta)
396 -> Meta = Meta1.put(previous, Previous)
397 ; Meta = Meta1
398 ).
399
400filter_meta(Dict0, HasID, Dict) :-
401 dict_pairs(Dict0, Tag, Pairs0),
402 filter_pairs(Pairs0, HasID, Pairs),
403 dict_pairs(Dict, Tag, Pairs).
404
405filter_pairs([], _, []).
406filter_pairs([K-V0|T0], HasID, [K-V|T]) :-
407 meta_allowed(K, HasID, Type),
408 filter_type(Type, V0, V),
409 !,
410 filter_pairs(T0, HasID, T).
411filter_pairs([_|T0], HasID, T) :-
412 filter_pairs(T0, HasID, T).
413
414meta_allowed(public, _, boolean).
415meta_allowed(example, _, boolean).
416meta_allowed(author, _, string).
417meta_allowed(avatar, false, string).
418meta_allowed(email, _, string).
419meta_allowed(title, _, string).
420meta_allowed(tags, _, list(string)).
421meta_allowed(description, _, string).
422meta_allowed(commit_message, _, string).
423meta_allowed(modify, _, list(atom)).
424
425filter_type(Type, V, V) :-
426 is_of_type(Type, V),
427 !.
428filter_type(list(Type), V0, V) :-
429 is_list(V0),
430 maplist(filter_type(Type), V0, V).
431filter_type(atom, V0, V) :-
432 atomic(V0),
433 atom_string(V, V0).
434
435filter_auth(Auth0, Auth) :-
436 auth_template(Auth),
437 Auth :< Auth0,
438 !.
439filter_auth(Auth, Auth).
440
441auth_template(_{identity:_, profile_id:_}).
442auth_template(_{profile_id:_}).
443auth_template(_{identity:_}).
444
445
463
464storage_get(Request, swish, Options) :-
465 swish_reply_config(Request, Options),
466 !.
467storage_get(Request, Format, Options) :-
468 storage_dir(Dir),
469 request_file_or_hash(Request, Dir, FileOrHash, Type),
470 Obj =.. [Type,FileOrHash],
471 authorized(gitty(download(Obj, Format)), Options),
472 storage_get(Format, Dir, Type, FileOrHash, Request),
473 broadcast(swish(download(Dir, FileOrHash, Format))).
474
475storage_get(swish, Dir, Type, FileOrHash, Request) :-
476 gitty_data_or_default(Dir, Type, FileOrHash, Code, Meta),
477 chat_count(Meta, Count),
478 swish_show([ code(Code),
479 file(FileOrHash),
480 st_type(gitty),
481 meta(Meta),
482 chat_count(Count)
483 ],
484 Request).
485storage_get(raw, Dir, Type, FileOrHash, _Request) :-
486 gitty_data_or_default(Dir, Type, FileOrHash, Code, Meta),
487 file_mime_type(Meta.name, MIME),
488 format('Content-type: ~w~n~n', [MIME]),
489 format('~s', [Code]).
490storage_get(json, Dir, Type, FileOrHash, _Request) :-
491 gitty_data_or_default(Dir, Type, FileOrHash, Code, Meta),
492 chat_count(Meta, Count),
493 JSON0 = json{data:Code, meta:Meta, chats:_{total:Count}},
494 ( open_hook(json, JSON0, JSON)
495 -> true
496 ; JSON = JSON0
497 ),
498 reply_json_dict(JSON).
499storage_get(history(Depth, Includes), Dir, _, File, _Request) :-
500 gitty_history(Dir, File, History, [depth(Depth),includes(Includes)]),
501 reply_json_dict(History).
502storage_get(history(Depth), Dir, _, File, _Request) :-
503 gitty_history(Dir, File, History, [depth(Depth)]),
504 reply_json_dict(History).
505storage_get(diff(RelTo), Dir, _, File, _Request) :-
506 gitty_diff(Dir, RelTo, File, Diff),
507 reply_json_dict(Diff).
508
509request_file_or_hash(Request, Dir, FileOrHash, Type) :-
510 option(path_info(FileOrHash), Request),
511 ( gitty_file(Dir, FileOrHash, _Hash)
512 -> Type = file
513 ; is_gitty_hash(FileOrHash)
514 -> Type = hash
515 ; gitty_default_file(FileOrHash, _)
516 -> Type = default
517 ; http_404([], Request)
518 ).
519
524
525gitty_data_or_default(_, default, File, Code,
526 meta{name:File,
527 modify:[login,owner],
528 default:true,
529 chat:"large"
530 }) :-
531 !,
532 gitty_default_file(File, Path),
533 read_file_to_string(Path, Code, []).
534gitty_data_or_default(Dir, _, FileOrHash, Code, Meta) :-
535 gitty_data(Dir, FileOrHash, Code, Meta),
536 !.
537
538gitty_default_file(File, Path) :-
539 file_name_extension(Base, Ext, File),
540 memberchk(Ext, [pl,swinb]),
541 forall(sub_atom(Base, _, 1, _, C),
542 char_type(C, csym)),
543 absolute_file_name(config(gitty/File), Path,
544 [ access(read),
545 file_errors(fail)
546 ]).
547
548
553
554chat_count(Meta, Chats) :-
555 atom_concat('gitty:', Meta.get(name), DocID),
556 swish_config:chat_count_about(DocID, Chats),
557 !.
558chat_count(_, 0).
559
560
564
565random_filename(Name) :-
566 length(Chars, 8),
567 maplist(random_char, Chars),
568 atom_chars(Name, Chars).
569
570from('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ').
571
572random_char(Char) :-
573 from(From),
574 atom_length(From, Len),
575 Max is Len - 1,
576 random_between(0, Max, I),
577 sub_atom(From, I, 1, _, Char).
578
579
584
585:- multifile open_hook/3. 586
587swish_show(Options0, Request) :-
588 open_hook(swish, Options0, Options),
589 !,
590 swish_reply(Options, Request).
591swish_show(Options, Request) :-
592 swish_reply(Options, Request).
593
594
595 598
609
610storage_file(File) :-
611 storage_file_extension(File, _).
612
613storage_file_extension(File, Ext) :-
614 storage_file_extension_head(File, Ext, _).
615
616storage_file_extension_head(File, Ext, Head) :-
617 open_gittystore(Dir),
618 gitty_file(Dir, File, Ext, Head).
619
620storage_file(File, Data, Meta) :-
621 open_gittystore(Dir),
622 ( var(File)
623 -> gitty_file(Dir, File, Head),
624 gitty_data(Dir, Head, Data, Meta)
625 ; gitty_data(Dir, File, Data, Meta)
626 ).
627
628storage_meta_data(File, Meta) :-
629 open_gittystore(Dir),
630 ( var(File)
631 -> gitty_file(Dir, File, _Head)
632 ; true
633 ),
634 gitty_commit(Dir, File, Meta).
635
640
641storage_commit(Hash, Meta) :-
642 open_gittystore(Dir),
643 gitty_plain_commit(Dir, Hash, Meta).
644
652
653storage_meta_property(Meta, Property) :-
654 current_meta_property(Property, How),
655 meta_property(Property, How, Meta).
656
657meta_property(Property, dict, Identity) :-
658 Property =.. [Name,Value],
659 Value = Identity.get(Name).
660meta_property(modify(Modify), _, Meta) :-
661 ( Modify0 = Meta.get(modify)
662 -> Modify = Modify0
663 ; Modify = [any,login,owner]
664 ).
665
666current_meta_property(peer(_Atom), dict).
667current_meta_property(public(_Bool), dict).
668current_meta_property(time(_Seconds), dict).
669current_meta_property(author(_String), dict).
670current_meta_property(identity(_String), dict).
671current_meta_property(avatar(_String), dict).
672current_meta_property(modify(_List), derived).
673
679
680storage_store_term(Term, Hash) :-
681 open_gittystore(Dir),
682 with_output_to(string(S), write_canonical(Term)),
683 gitty_save(Dir, S, term, Hash).
684
685storage_load_term(Hash, Term) :-
686 open_gittystore(Dir),
687 gitty_load(Dir, Hash, Data, term),
688 term_string(Term, Data).
689
690
691 694
703
704use_gitty_file(File) :-
705 use_gitty_file(File, []).
706
707use_gitty_file(M:Spec, Options) :-
708 ensure_extension(Spec, pl, File),
709 setup_watch(M:File, Options),
710 storage_file(File, Data, Meta),
711 atom_concat('swish://', File, URL),
712 setup_call_cleanup(
713 open_string(Data, In),
714 load_files(M:URL,
715 [ stream(In),
716 modified(Meta.time),
717 if(changed)
718 | Options
719 ]),
720 close(In)).
721
722ensure_extension(File, Ext, File) :-
723 file_name_extension(_, Ext, File),
724 !.
725ensure_extension(Base, Ext, File) :-
726 file_name_extension(Base, Ext, File).
727
728
729:- dynamic
730 watching/3. 731
732setup_watch(M:File, Options) :-
733 option(watch(true), Options, true),
734 !,
735 ( watching(File, M, Options)
736 -> true
737 ; retractall(watching(File, M, _)),
738 assertz(watching(File, M, Options))
739 ).
740setup_watch(M:File, _Options) :-
741 retractall(watching(File, M, _)).
742
743
744 747
748:- initialization
749 listen(swish(updated(File, Commit)),
750 run_watchdog(File, Commit)). 751
752run_watchdog(File, _Commit) :-
753 debug(gitty(reload), 'File ~p was saved', [File]),
754 forall(watching(File, Module, Options),
755 use_gitty_file(Module:File, Options)).
756
757
758 761
766
767:- meta_predicate
768 collect_messages_as_json(0, -). 769
770:- thread_local
771 messages/1. 772
773collect_messages_as_json(Goal, Messages) :-
774 retractall(messages(_)),
775 setup_call_cleanup(
776 asserta((user:thread_message_hook(Term,Kind,Lines) :-
777 collect_message(Term,Kind,Lines)),
778 Ref),
779 Goal,
780 erase(Ref)),
781 findall(Msg, retract(messages(Msg)), Messages).
782
783collect_message(Term, Kind, Lines) :-
784 message_to_json(Term, Kind, Lines, JSON),
785 assertz(messages(JSON)).
786
787message_to_json(Term, Kind, Lines, JSON) :-
788 message_to_string(Term, String),
789 JSON0 = json{type: message,
790 kind: Kind,
791 data: [String]},
792 add_html_message(Kind, Lines, JSON0, JSON1),
793 ( source_location(File, Line)
794 -> JSON2 = JSON1.put(location, json{file:File, line:Line})
795 ; JSON2 = JSON1
796 ),
797 ( message_details(Term, JSON2, JSON)
798 -> true
799 ; JSON = JSON2
800 ).
801
802message_details(error(syntax_error(_What),
803 file(File,Line,Offset,_CharPos)),
804 JSON0, JSON) :-
805 JSON = JSON0.put(location, json{file:File, line:Line, ch:Offset})
806 .put(code, syntax_error).
807message_details(load_file(Step), JSON0, JSON) :-
808 functor(Step, Code, _),
809 JSON = JSON0.put(code, Code).
810
812:- if(current_predicate(message_lines_to_html/3)). 813add_html_message(Kind, Lines, JSON0, JSON) :-
814 atom_concat('msg-', Kind, Class),
815 message_lines_to_html(Lines, [Class], HTML),
816 JSON = JSON0.put(html, HTML).
817:- else. 818add_html_message(_, _, JSON, JSON).
819:- endif. 820
821 824
828
829storage_fsck :-
830 open_gittystore(Dir),
831 gitty_fsck(Dir).
832
839
840:- multifile
841 gitty_driver_files:repack_objects/2,
842 gitty_driver_files:unpack_packs/1. 843
844storage_repack :-
845 storage_repack([]).
846storage_repack(Options) :-
847 open_gittystore(Dir),
848 ( gitty_driver(Dir, files)
849 -> gitty_driver_files:repack_objects(Dir, Options)
850 ; print_message(informational, gitty(norepack(driver)))
851 ).
852
858
859storage_unpack :-
860 open_gittystore(Dir),
861 ( gitty_driver(Dir, files)
862 -> gitty_driver_files:unpack_packs(Dir)
863 ; print_message(informational, gitty(nounpack(driver)))
864 ).
865
866
867 870
871:- multifile
872 swish_search:typeahead/4. 873
886
887swish_search:typeahead(file, Query, FileInfo, _Options) :-
888 open_gittystore(Dir),
889 gitty_file(Dir, File, Head),
890 gitty_plain_commit(Dir, Head, Meta),
891 Meta.get(public) == true,
892 ( sub_atom(File, 0, _, _, Query) 893 -> true
894 ; meta_match_query(Query, Meta)
895 -> true
896 ),
897 FileInfo = Meta.put(_{type:"store", file:File}).
898
899meta_match_query(Query, Meta) :-
900 member(Tag, Meta.get(tags)),
901 sub_atom(Tag, 0, _, _, Query).
902meta_match_query(Query, Meta) :-
903 sub_atom(Meta.get(author), 0, _, _, Query).
904meta_match_query(Query, Meta) :-
905 Title = Meta.get(title),
906 sub_atom_icasechk(Title, Start, Query),
907 ( Start =:= 0
908 -> true
909 ; Before is Start-1,
910 sub_atom(Title, Before, 1, _, C),
911 \+ char_type(C, csym)
912 ).
913
914swish_search:typeahead(store_content, Query, FileInfo, Options) :-
915 limit(25, search_store_content(Query, FileInfo, Options)).
916
917search_store_content(Query, FileInfo, Options) :-
918 open_gittystore(Dir),
919 gitty_file(Dir, File, Head),
920 gitty_data(Dir, Head, Data, Meta),
921 Meta.get(public) == true,
922 limit(5, search_file(File, Meta, Data, Query, FileInfo, Options)).
923
924search_file(File, Meta, Data, Query, FileInfo, Options) :-
925 split_string(Data, "\n", "\r", Lines),
926 nth1(LineNo, Lines, Line),
927 match(Line, Query, Options),
928 FileInfo = Meta.put(_{type:"store", file:File,
929 line:LineNo, text:Line, query:Query
930 }).
931
932
933 936
971
972
973source_list(Request) :-
974 memberchk(method(options), Request),
975 !,
976 cors_enable(Request,
977 [ methods([get,post])
978 ]),
979 format('~n').
980source_list(Request) :-
981 cors_enable,
982 authenticate(Request, Auth),
983 http_parameters(Request,
984 [ q(Q, [optional(true)]),
985 o(Order, [ oneof([time,name,author,type]),
986 default(time)
987 ]),
988 offset(Offset, [integer, default(0)]),
989 limit(Limit, [integer, default(10)]),
990 display_name(DisplayName, [optional(true), string]),
991 avatar(Avatar, [optional(true), string])
992 ]),
993 bound(Auth.put(_{display_name:DisplayName, avatar:Avatar}), AuthEx),
994 order(Order, Field, Cmp),
995 last_modified(Modified),
996 statistics(cputime, CPU0),
997 findall(Source, source(Q, AuthEx, Source), AllSources),
998 statistics(cputime, CPU1),
999 length(AllSources, Count),
1000 CPU is CPU1 - CPU0,
1001 sort(Field, Cmp, AllSources, Ordered),
1002 list_offset_limit(Ordered, Offset, Limit, Sources),
1003 reply_json_dict(json{total:Count, offset:Offset,
1004 cpu:CPU, modified:Modified,
1005 matches:Sources}).
1006
1007list_offset_limit(List0, Offset, Limit, List) :-
1008 list_offset(List0, Offset, List1),
1009 list_limit(List1, Limit, List).
1010
1011list_offset([_|T0], Offset, T) :-
1012 succ(O1, Offset),
1013 !,
1014 list_offset(T0, O1, T).
1015list_offset(List, _, List).
1016
1017list_limit([H|T0], Limit, [H|T]) :-
1018 succ(L1, Limit),
1019 !,
1020 list_limit(T0, L1, T).
1021list_limit(_, _, []).
1022
1023order(type, ext, @=<) :- !.
1024order(time, time, @>=) :- !.
1025order(Field, Field, @=<).
1026
1027source(Q, Auth, Source) :-
1028 parse_query(Q, Query),
1029 source_q(Query, Auth, Source).
1030
1031source_q([user("me")], Auth, _Source) :-
1032 \+ _ = Auth.get(avatar),
1033 \+ user_property(Auth, identity(_Id)),
1034 !,
1035 fail.
1036source_q(Query, Auth, Source) :-
1037 type_constraint(Query, Query1, Type),
1038 partition(content_query, Query1,
1039 ContentConstraints, MetaConstraints),
1040 storage_file_extension_head(File, Type, Head),
1041 source_data(File, Head, Meta, Source),
1042 visible(Meta, Auth, MetaConstraints),
1043 maplist(matches_meta(Source, Auth), MetaConstraints),
1044 matches_content(ContentConstraints, Head).
1045
1046content_query(string(_)).
1047content_query(regex(_)).
1048
1049source_data(File, Head, Meta, Source) :-
1050 storage_commit(Head, Meta),
1051 file_name_extension(_, Type, File),
1052 Info = _{time:_, tags:_, author:_, avatar:_, name:_},
1053 Info >:< Meta,
1054 bound(Info, Info2),
1055 Source = Info2.put(_{type:st_gitty, ext:Type}).
1056
1057bound(Dict0, Dict) :-
1058 dict_pairs(Dict0, Tag, Pairs0),
1059 include(bound, Pairs0, Pairs),
1060 dict_pairs(Dict, Tag, Pairs).
1061
1062bound(_-V) :- nonvar(V).
1063
1065
1066visible(Meta, Auth, Constraints) :-
1067 memberchk(user("me"), Constraints),
1068 !,
1069 owns(Auth, Meta, user(_)).
1070visible(Meta, _Auth, _Constraints) :-
1071 Meta.get(public) == true,
1072 !.
1073visible(Meta, Auth, _Constraints) :-
1074 owns(Auth, Meta, _).
1075
1084
1085owns(Auth, Meta, user(me)) :-
1086 storage_meta_property(Meta, identity(Id)),
1087 !,
1088 user_property(Auth, identity(Id)).
1089owns(_Auth, Meta, _) :- 1090 \+ Meta.get(public) == true, 1091 !,
1092 fail.
1093owns(Auth, Meta, user(avatar)) :-
1094 storage_meta_property(Meta, avatar(Id)),
1095 user_property(Auth, avatar(Id)),
1096 !.
1097owns(Auth, Meta, user(nickname)) :-
1098 Auth.get(display_name) == Meta.get(author),
1099 !.
1100owns(Auth, Meta, host(How)) :- 1101 Peer = Auth.get(peer),
1102 ( Peer == Meta.get(peer)
1103 -> How = same
1104 ; sub_atom(Meta.get(peer), 0, _, _, '127.0.0.')
1105 -> How = local
1106 ).
1107
1111
1112matches_meta(Dict, _, tag(Tag)) :-
1113 !,
1114 ( Tag == ""
1115 -> Dict.get(tags) \== []
1116 ; member(Tagged, Dict.get(tags)),
1117 match_meta(Tag, Tagged)
1118 -> true
1119 ).
1120matches_meta(Dict, _, name(Name)) :-
1121 !,
1122 match_meta(Name, Dict.get(name)).
1123matches_meta(Dict, _, user(Name)) :-
1124 ( Name \== "me"
1125 -> match_meta(Name, Dict.get(author))
1126 ; true 1127 ).
1128
1129match_meta(regex(RE), Value) :-
1130 !,
1131 re_match(RE, Value).
1132match_meta(String, Value) :-
1133 sub_atom_icasechk(Value, _, String).
1134
1135matches_content([], _) :- !.
1136matches_content(Constraints, Hash) :-
1137 storage_file(Hash, Data, _Meta),
1138 maplist(match_content(Data), Constraints).
1139
1140match_content(Data, string(S)) :-
1141 sub_atom_icasechk(Data, _, S),
1142 !.
1143match_content(Data, regex(RE)) :-
1144 re_match(RE, Data).
1145
1150
1151type_constraint(Query0, Query, Type) :-
1152 partition(is_type, Query0, Types, Query),
1153 ( Types == []
1154 -> true
1155 ; Types = [type(Type)]
1156 -> true
1157 ; maplist(arg(1), Types, List),
1158 freeze(Type, memberchk(Type, List))
1159 ).
1160
1161is_type(type(_)).
1162
1169
1170parse_query(Q, Query) :-
1171 var(Q),
1172 !,
1173 Query = [].
1174parse_query(Q, Query) :-
1175 string_codes(Q, Codes),
1176 phrase(query(Query), Codes).
1177
1178query([H|T]) -->
1179 blanks,
1180 query1(H),
1181 !,
1182 query(T).
1183query([]) -->
1184 blanks.
1185
1186query1(Q) -->
1187 tag(Tag, Value),
1188 !,
1189 {Q =.. [Tag,Value]}.
1190query1(Q) -->
1191 "\"", string(Codes), "\"",
1192 !,
1193 { string_codes(String, Codes),
1194 Q = string(String)
1195 }.
1196query1(Q) -->
1197 "/", string(Codes), "/", re_flags(Flags),
1198 !,
1199 { string_codes(String, Codes),
1200 re_compile(String, RE, Flags),
1201 Q = regex(RE)
1202 }.
1203query1(Q) -->
1204 next_word(String),
1205 { String \== "",
1206 re_compile(String, RE,
1207 [ extended(true),
1208 caseless(true)
1209 ]),
1210 Q = regex(RE)
1211 }.
1212
1213re_flags([H|T]) -->
1214 re_flag(H),
1215 !,
1216 re_flags(T).
1217re_flags([]) -->
1218 blank.
1219re_flags([]) -->
1220 eos.
1221
1222re_flag(caseless(true)) --> "i".
1223re_flag(extended(true)) --> "x".
1224re_flag(multiline(true)) --> "m".
1225re_flag(dotall(true)) --> "s".
1226
1227next_word(String) -->
1228 blanks, nonblank(H), string(Codes), ( blank ; eos ),
1229 !,
1230 { string_codes(String, [H|Codes]) }.
1231
1232tag(name, Value) --> "name:", tag_value(Value, _).
1233tag(tag, Value) --> "tag:", tag_value(Value, _).
1234tag(user, Value) --> "user:", tag_value(Value, _).
1235tag(type, Value) --> "type:", tag_value(String, string(_)), { atom_string(Value, String) }.
1236
1237tag_value(String, string(quoted)) -->
1238 blanks, "\"", !, string(Codes), "\"",
1239 !,
1240 { string_codes(String, Codes) }.
1241tag_value(Q, regex) -->
1242 blanks, "/", string(Codes), "/", re_flags(Flags),
1243 !,
1244 { Codes == []
1245 -> Q = ""
1246 ; string_codes(String, Codes),
1247 re_compile(String, RE, Flags),
1248 Q = regex(RE)
1249 }.
1250tag_value(String, string(nonquoted)) -->
1251 nonblank(H),
1252 !,
1253 string(Codes),
1254 ( blank ; eos ),
1255 !,
1256 { string_codes(String, [H|Codes]) }.
1257tag_value("", empty) -->
1258 "".
1259
1260 1263
1272
1273source_modified(Request) :-
1274 memberchk(method(options), Request),
1275 !,
1276 cors_enable(Request,
1277 [ methods([get])
1278 ]),
1279 format('~n').
1280source_modified(Request) :-
1281 cors_enable,
1282 authenticate(Request, _Auth),
1283 last_modified(Time),
1284 reply_json_dict(json{modified:Time}).
1285
1286:- dynamic gitty_last_modified/1. 1287
1288update_last_modified(_,_) :-
1289 with_mutex(gitty_last_modified,
1290 update_last_modified_sync).
1291
1292update_last_modified_sync :-
1293 get_time(Now),
1294 retractall(gitty_last_modified(_)),
1295 asserta(gitty_last_modified(Now)).
1296
1297last_modified(Time) :-
1298 debugging(swish(sourcelist)), 1299 !,
1300 get_time(Now),
1301 Time is Now + 60.
1302last_modified(Time) :-
1303 with_mutex(gitty_last_modified,
1304 last_modified_sync(Time)).
1305
1306last_modified_sync(Time) :-
1307 ( gitty_last_modified(Time)
1308 -> true
1309 ; statistics(process_epoch, Time)
1310 ).
1311
1312:- unlisten(swish(_)),
1313 listen(swish(Event), notify_event(Event)). 1314
1316notify_event(updated(File, Commit)) :-
1317 atom_concat('gitty:', File, DocID),
1318 update_last_modified(Commit, DocID).
1319notify_event(deleted(File, Commit)) :-
1320 atom_concat('gitty:', File, DocID),
1321 update_last_modified(Commit, DocID).
1322notify_event(created(File, Commit)) :-
1323 atom_concat('gitty:', File, DocID),
1324 update_last_modified(Commit, DocID).
1325
1326
1327 1330
1331:- multifile prolog:message//1. 1332
1333prolog:message(moved_old_store(Old, New)) -->
1334 [ 'Moving SWISH file store from ~p to ~p'-[Old, New] ]