123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425(*
* Copyright (c) 2013-2017 Thomas Gazagnaire <thomas@gazagnaire.org>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*)open!ImportopenIrmin_http_commonmoduleT=Irmin.Typeletto_json=Irmin.Type.to_json_stringletof_json=Irmin.Type.of_json_stringletsrc=Logs.Src.create"irmin.http-srv"~doc:"Irmin REST API server"moduleLog=(valLogs.src_logsrc:Logs.LOG)moduletypeS=sigtyperepotypetvalv:?strict:bool->repo->tendmoduleMake(HTTP:Cohttp_lwt.S.Server)(S:Irmin.S)=structmoduleWm=structmoduleRd=Webmachine.RdmoduleClock=structletnow()=int_of_float(Unix.gettimeofday())endincludeWebmachine.Make(HTTP.IO)(Clock)endmoduleP=S.Privateclassvirtualresource=objectinherit[Cohttp_lwt.Body.t]Wm.resourcemethod!finish_requestrd=Wm.Rd.with_resp_headers(funh->Cohttp.Header.addhirmin_versionIrmin.version)rd|>Wm.continue()end(** A [POST] endpoint that performs a given repository operation. *)classpost_unit_endpoint~oprepo=objectinheritresourcemethod!allowed_methodsrd=Wm.continue[`POST]rdmethodcontent_types_providedrd=Wm.continue[("application/json",fun_->assertfalse)]rdmethodcontent_types_acceptedrd=Wm.continue[]rdmethod!process_postrd=let*_=Cohttp_lwt.Body.to_stringrd.Wm.Rd.req_bodyinoprepo>>=fun()->letresp_body=`String""inWm.continuetrue{rdwithWm.Rd.resp_body}endletparse_errorrdstr(`Msge)=leterr=Fmt.str"Parse error %S: %s"streinWm.respond~body:(`Stringerr)400rdmoduleContent_addressable(S:sigincludeIrmin.CONTENT_ADDRESSABLE_STOREvalbatch:P.Repo.t->(read_writet->'aLwt.t)->'aLwt.tend)(K:Irmin.Type.Swithtypet=S.key)(V:Irmin.Type.Swithtypet=S.value)=structletwith_keyrdf=matchIrmin.Type.of_stringK.t(Wm.Rd.lookup_path_info_exn"id"rd)with|Okkey->fkey|Error_->Wm.respond404rdletaddrdrepo=let*body=Cohttp_lwt.Body.to_stringrd.Wm.Rd.req_bodyinmatchIrmin.Type.of_stringV.tbodywith|Errore->parse_errorrdbodye|Okbody->S.batchrepo@@fundb->let*new_id=S.adddbbodyinletresp_body=`String(Irmin.Type.to_stringK.tnew_id)inWm.continuetrue{rdwithWm.Rd.resp_body}letunsafe_addrdrepokey=let*body=Cohttp_lwt.Body.to_stringrd.Wm.Rd.req_bodyinmatchIrmin.Type.of_stringV.tbodywith|Errore->parse_errorrdbodye|Okbody->S.batchrepo@@fundb->S.unsafe_adddbkeybody>>=fun()->letresp_body=`String""inWm.continuetrue{rdwithWm.Rd.resp_body}classitemsrepo=objectinheritresourcemethod!allowed_methodsrd=Wm.continue[`POST]rdmethodcontent_types_providedrd=Wm.continue[("application/json",fun_->assertfalse)]rdmethodcontent_types_acceptedrd=Wm.continue[]rdmethod!process_postrd=addrdrepoendclassunsafe_itemsrepo=objectinheritresourcemethod!allowed_methodsrd=Wm.continue[`POST]rdmethodcontent_types_providedrd=Wm.continue[("application/json",fun_->assertfalse)]rdmethodcontent_types_acceptedrd=Wm.continue[]rdmethod!process_postrd=with_keyrd(unsafe_addrdrepo)endclassmergemergerepo=objectinheritresourcemethod!allowed_methodsrd=Wm.continue[`POST]rdmethodcontent_types_providedrd=Wm.continue[("application/json",fun_->assertfalse)]rdmethodcontent_types_acceptedrd=Wm.continue[]rdmethod!process_postrd=let*body=Cohttp_lwt.Body.to_stringrd.Wm.Rd.req_bodyinmatchIrmin.Type.(of_string(merge_t(optionK.t)))bodywith|Errore->parse_errorrdbodye|Ok{old;left;right}->S.batchrepo@@fundb->letold=Irmin.Merge.promiseoldinlet*m=Irmin.Merge.f(mergedb)~oldleftrightinletresult=Irmin.Merge.result_t(Irmin.Type.optionK.t)inletresp_body=`StringIrmin.(Type.to_stringresultm)inWm.continuetrue{rdwithWm.Rd.resp_body}endclassitemdb=object(self)inheritresourcemethodprivateto_jsonrd=with_keyrd(funkey->letstr=Irmin.Type.to_stringV.tinS.finddbkey>>=function|Somevalue->Wm.continue(`String(strvalue))rd|None->Wm.respond404rd)method!allowed_methodsrd=Wm.continue[`GET;`HEAD]rdmethodcontent_types_acceptedrd=Wm.continue[]rdmethod!resource_existsrd=with_keyrd(funkey->let*mem=S.memdbkeyinWm.continuememrd)methodcontent_types_providedrd=Wm.continue[("application/json",self#to_json)]rdendclassclear=post_unit_endpoint~op:S.clearendmoduleAtomic_write(S:Irmin.ATOMIC_WRITE_STORE)(K:Irmin.Type.Swithtypet=S.key)(V:Irmin.Type.Swithtypet=S.value)=structclassitemsdb=object(self)inheritresourcemethod!allowed_methodsrd=Wm.continue[`GET;`HEAD]rdmethodcontent_types_acceptedrd=Wm.continue[]rdmethodprivateto_jsonrd=let*keys=S.listdbinletjson=to_jsonT.(listK.t)keysinWm.continue(`Stringjson)rdmethodcontent_types_providedrd=Wm.continue[("application/json",self#to_json)]rdendletwith_keyrdf=matchIrmin.Type.of_stringK.trd.Wm.Rd.dispatch_pathwith|Okx->fx|Error_->Wm.respond404rdclassitemdb=object(self)inheritresourcemethodprivateof_jsonrd=let*body=Cohttp_lwt.Body.to_stringrd.Wm.Rd.req_bodyinmatchof_json(set_tV.t)bodywith|Errore->parse_errorrdbodye|Okv->with_keyrd(funkey->matchv.vwith|Somev->S.setdbkeyv>>=fun()->letresp_body=`String(to_jsonstatus_t"ok")inletrd={rdwithWm.Rd.resp_body}inWm.continuetruerd|None->let*b=S.test_and_setdbkey~test:v.test~set:v.setinletresp_body=`String(to_jsonstatus_t(string_of_boolb))inletrd={rdwithWm.Rd.resp_body}inWm.continuebrd)methodprivateto_jsonrd=with_keyrd(funkey->letstr=Irmin.Type.to_stringV.tinS.finddbkey>>=function|Somevalue->Wm.continue(`String(strvalue))rd|None->Wm.respond404rd)method!resource_existsrd=with_keyrd(funkey->let*mem=S.memdbkeyinWm.continuememrd)method!allowed_methodsrd=Wm.continue[`GET;`HEAD;`PUT;`DELETE]rdmethodcontent_types_providedrd=Wm.continue[("application/json",self#to_json)]rdmethodcontent_types_acceptedrd=Wm.continue[("application/json",self#of_json)]rdmethod!delete_resourcerd=with_keyrd(funkey->S.removedbkey>>=fun()->letresp_body=`String(to_jsonstatus_t"ok")inWm.continuetrue{rdwithWm.Rd.resp_body})endclasswatchesdb=object(self)inheritresourcemethod!allowed_methodsrd=Wm.continue[`GET;`HEAD;`POST]rdmethodcontent_types_acceptedrd=Wm.continue[]rdmethodprivatestream?init()=letstream,push=Lwt_stream.create()inlet+w=S.watch?initdb(funkeydiff->letv=to_json(event_tK.tV.t)(key,diff)inpush(Somev);push(Some",");Lwt.return_unit)inLwt.async(fun()->Lwt_stream.closedstream>>=fun()->S.unwatchdbw);push(Some"[");`Streamstreammethod!process_postrd=let*body=Cohttp_lwt.Body.to_stringrd.Wm.Rd.req_bodyinmatchof_jsonT.(list(init_tK.tV.t))bodywith|Errore->parse_errorrdbodye|Okinit->let*resp_body=self#stream~init()inWm.continuetrue{rdwithWm.Rd.resp_body}methodprivateof_jsonrd=let*body=self#stream()inWm.continuebodyrdmethodcontent_types_providedrd=Wm.continue[("application/json",self#of_json)]rdendclasswatchdb=object(self)inheritresourcemethod!allowed_methodsrd=Wm.continue[`GET;`HEAD;`POST]rdmethodcontent_types_acceptedrd=Wm.continue[]rdmethodprivatestream?initkey=letstream,push=Lwt_stream.create()inlet+w=S.watch_key?initdbkey(fundiff->letv=to_json(event_tK.tV.t)(key,diff)inpush(Somev);push(Some",");Lwt.return_unit)inLwt.async(fun()->Lwt_stream.closedstream>>=fun()->S.unwatchdbw);push(Some"[");`Streamstreammethod!process_postrd=let*body=Cohttp_lwt.Body.to_stringrd.Wm.Rd.req_bodyinmatchof_jsonV.tbodywith|Errore->parse_errorrdbodye|Okinit->with_keyrd(funkey->let*resp_body=self#stream~initkeyinWm.continuetrue{rdwithWm.Rd.resp_body})methodprivateof_jsonrd=with_keyrd(funkey->let*body=self#streamkeyinWm.continuebodyrd)methodcontent_types_providedrd=Wm.continue[("application/json",self#of_json)]rdendclassclear=post_unit_endpoint~op:S.clearendmoduleBlob=Content_addressable(structincludeP.Contentsletbatchtf=P.Repo.batcht@@funx__->fxend)(P.Contents.Key)(P.Contents.Val)moduleTree=Content_addressable(structincludeP.Nodeletbatchtf=P.Repo.batcht@@fun_x_->fxend)(P.Node.Key)(P.Node.Val)moduleCommit=Content_addressable(structincludeP.Commitletbatchtf=P.Repo.batcht@@fun__x->fxend)(P.Commit.Key)(P.Commit.Val)moduleBranch=Atomic_write(P.Branch)(P.Branch.Key)(P.Branch.Val)typerepo=S.Repo.ttypet=HTTP.tletv?strict:_db=letblob=P.Repo.contents_tdbinlettree=P.Repo.node_tdbinletcommit=P.Repo.commit_tdbinletbranch=P.Repo.branch_tdbinletroutes=[("/blobs",fun()->newBlob.itemsdb);("/blob/:id",fun()->newBlob.itemblob);("/trees",fun()->newTree.itemsdb);("/trees/merge",fun()->newTree.mergeS.Private.Node.mergedb);("/tree/:id",fun()->newTree.itemtree);("/commits",fun()->newCommit.itemsdb);("/commit/:id",fun()->newCommit.itemcommit);("/unsafe/blobs/:id",fun()->newBlob.unsafe_itemsdb);("/unsafe/trees/:id",fun()->newTree.unsafe_itemsdb);("/unsafe/commits/:id",fun()->newCommit.unsafe_itemsdb);("/branches",fun()->newBranch.itemsbranch);("/branch/*",fun()->newBranch.itembranch);("/watches",fun()->newBranch.watchesbranch);("/watch/*",fun()->newBranch.watchbranch);("/clear/blobs",fun()->newBlob.clearblob);("/clear/trees",fun()->newTree.cleartree);("/clear/commits",fun()->newCommit.clearcommit);("/clear/branches",fun()->newBranch.clearbranch);]inletpp_con=Fmt.of_to_stringCohttp.Connection.to_stringinletcallback(_ch,conn)requestbody=letopenCohttpinLog.debug(funl->l"new connection %a"pp_conconn);let*status,headers,body,_path=Wm.dispatch'routes~body~request>|=function|None->(`Not_found,Header.init(),`String"Not found",[])|Someresult->resultinLog.info(funl->l"[%a] %d - %s %s"pp_conconn(Code.code_of_statusstatus)(Code.string_of_method(Request.methrequest))(Uri.path(Request.urirequest)));(* Finally, send the response to the client *)HTTP.respond~headers~body~status()in(* create the server and handle requests with the function defined above *)letconn_closed(_,conn)=Log.debug(funl->l"connection %a closed"pp_conconn)inHTTP.make~callback~conn_closed()end