123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246openLwt.Infix(* in LittleFS, the superblock and the root of the filesystem
* is always at a constant pair - addresses (0, 1) *)letroot_pair=(0L,1L)moduleMake(Sectors:Mirage_block.S)(Clock:Mirage_clock.PCLOCK)=structmoduleFs=Fs.Make(Sectors)(Clock)typekey=Mirage_kv.Key.tletlog_src=Logs.Src.create"chamelon-kv"~doc:"chamelon KV layer"moduleLog=(valLogs.src_loglog_src:Logs.LOG)(* error type definitions straight outta mirage-kv *)typeerror=[|`Not_foundofkey(** key not found *)|`Dictionary_expectedofkey(** key does not refer to a dictionary. *)|`Value_expectedofkey(** key does not refer to a value. *)]typewrite_error=[|error|`No_space(** No space left on the device. *)|`Too_many_retriesofint(** {!batch} has been trying to commit [n] times
without success. *)]letpp_errorfmt=function|`Not_foundkey->Format.fprintffmt"key %a not found"Mirage_kv.Key.ppkey|`Dictionary_expectedkey->Format.fprintffmt"%a was not a dictionary"Mirage_kv.Key.ppkey|`Value_expectedkey->Format.fprintffmt"%a was not a value"Mirage_kv.Key.ppkeyletpp_write_errorfmt=function|`No_space->Format.fprintffmt"no space left on device"|`Too_many_retriesn->Format.fprintffmt"tried to write %d times and didn't succeed"n|#errorase->pp_errorfmtetypet=Fs.tletget=Fs.File_read.getletget_partial=Fs.File_read.get_partial(* [set] does a little work on top of the filesystem's set functions, because
* we need to make directories if the key has >1 segment in it. *)(* Once we've either found or created the parent directory, we can ask the FS layer
* to set the data appropriately there. *)letsettkeydata:(unit,write_error)resultLwt.t=letname_length=String.length@@Mirage_kv.Key.basenamekeyinifname_length>(Int32.to_intt.Fs.name_length_max)thenbeginLog.err(funf->f"key length %d exceeds max length %ld - refusing to write"name_lengtht.Fs.name_length_max);Lwt.return@@Error(`Not_foundMirage_kv.Key.empty)endelsebeginletdir=Mirage_kv.Key.parentkeyinFs.Find.find_first_blockpair_of_directorytroot_pair(Mirage_kv.Key.segmentsdir)>>=function|`Basename_onblock_pair->Log.debug(funm->m"found basename of path %a on block pair %Ld, %Ld"Mirage_kv.Key.ppkey(fstblock_pair)(sndblock_pair));(* the directory already exists, so just write the file *)Fs.File_write.set_in_directoryblock_pairt(Mirage_kv.Key.basenamekey)data|`No_idpath->beginLog.debug(funm->m"path component %s had no id; making it and its children"path);(* something along the path is missing, so make it. *)(* note we need to call mkdir with the whole path (except for the basename),
* so that we get all levels of directory we may need,
* not just the first thing that was found missing. *)Fs.mkdirtroot_pair(Mirage_kv.Key.segmentsdir)>>=function|Error(`Not_found_)->Lwt.return@@(Error(`Not_found(Mirage_kv.Key.vpath)))|Error`No_spacease->Lwt.returne|Okblock_pair->Log.debug(funm->m"made filesystem structure for %a, writing to blockpair %Ld, %Ld"Mirage_kv.Key.ppdir(fstblock_pair)(sndblock_pair));Fs.File_write.set_in_directoryblock_pairt(Mirage_kv.Key.basenamekey)dataend(* No_structs represents an inconsistent on-disk structure.
* We can't do the right thing, so we return an error. *)|`No_structs->Log.err(funm->m"id was present but no matching directory structure");Lwt.return@@Error(`Not_foundkey)end(** [list t key], where [key] is a reachable directory,
* gives the files and directories (values and dictionaries) in [key].
* It is not a recursive listing. *)letlisttkey:((string*[`Dictionary|`Value])list,error)resultLwt.t=letcmp(name1,_)(name2,_)=String.comparename1name2in(* once we've found the (first) directory pair of the *parent* directory,
* get the list of all entries naming files or directories
* and sort them *)letls_in_dirdir_pair=Fs.Find.all_entries_in_dirtdir_pair>>=function|Error_->Lwt.return@@Error(`Not_foundkey)|Okentries_by_block->lettranslateentries=List.filter_mapChamelon.Entry.info_of_entryentries|>List.sortcmpin(* we have to compact first, because IDs are unique per *block*, not directory.
* If we compact after flattening the list, we might wrongly conflate multiple
* entries in the same directory, but on different blocks. *)letcompacted=List.map(fun(_block,entries)->Chamelon.Entry.compactentries)entries_by_blockinLwt.return@@Ok(translate@@List.flattencompacted)in(* find the parent directory of the [key] *)match(Mirage_kv.Key.segmentskey)with|[]->ls_in_dirroot_pair|segments->(* descend into each segment until we run out, at which point we'll be in the
* directory we want to list *)Fs.Find.find_first_blockpair_of_directorytroot_pairsegments>>=function|`No_idk->(* be sure to return `k` as the error value, so the user might find out
* which part of a complex path is missing and be more easily able to fix the problem *)Lwt.return@@Error(`Not_found(Mirage_kv.Key.vk))(* No_structs is returned if part of the path is present, but not a directory (usually meaning
* it's a file instead) *)|`No_structs->Lwt.return@@Error(`Not_foundkey)|`Basename_onpair->ls_in_dirpair(** [exists t key] returns true *only* for a file/value called (basename key) set in (dirname key).
* A directory/dictionary doesn't cut it. *)letexiststkey=listt(Mirage_kv.Key.parentkey)>>=function|Error_ase->Lwt.returne|Okl->letlookup(name,dict_or_val)=if(String.comparename(Mirage_kv.Key.basenamekey))=0thenSomedict_or_valelseNoneinLwt.return@@Ok(List.find_maplookupl)letsizetkey=Fs.Size.sizetkeyletremovetkey=ifMirage_kv.Key.(equalemptykey)thenbegin(* it's impossible to remove the root directory in littlefs, as it's
* implicitly at the root pair *)Log.warn(funm->m"refusing to delete the root directory");Lwt.return@@Error(`Not_foundkey)endelse(* first, find the parent directory from which to delete (basename key) *)Fs.Find.find_first_blockpair_of_directorytroot_pairMirage_kv.Key.(segments@@parentkey)>>=function|`Basename_onpair->Log.debug(funf->f"found %a in a directory starting at %a, will delete"Mirage_kv.Key.ppkeyFmt.(pair~sep:commaint64int64)pair);Fs.Delete.delete_in_directorypairt(Mirage_kv.Key.basenamekey)(* if we couldn't find (parent key), it's already pretty deleted *)|`No_id_|`No_structs->Lwt.return@@Ok()(* [last_modified t key] gives the timestamp metadata for a file/value,
* or (for a directory) the most recently modified file/value within the directory.
* We don't have to recurse, thankfully, so we only have to examine files. *)letlast_modifiedtkey=(* figure out whether [key] represents a directory. *)Fs.Find.find_first_blockpair_of_directorytroot_pair(Mirage_kv.Key.segmentskey)>>=function|`No_id_|`No_structs->(* [key] either doesn't exist or is a value; Fs.last_modified_value handles both *)Fs.last_modified_valuetkey|`Basename_on_block_pair->(* we were asked to get the last_modified time of a directory :/ *)letopenLwt_result.Infixinlisttkey>>=funl->(* luckily, the spec says we should only check last_modified dates to a depth of 1 *)(* unfortunately, the spec *doesn't* say what the last_modified time of an empty directory is :/ *)(* it's convenient for us to say it's the earliest possible time,
* such that our fold can just use the latest time it's seen in the list as the accumulator *)Lwt_list.fold_left_s(funspanentry->matchspanwith|Error_ase->Lwt.returne|Okprev->matchentrywith|_,`Dictionary->Lwt.return(Okprev)|(name,`Value)->Fs.last_modified_valuetMirage_kv.Key.(key/name)>>=funnew_span->matchPtime.Span.of_d_psprev,Ptime.Span.of_d_psnew_spanwith|None,_|_,None->Lwt.return@@Error(`Not_foundkey)|Somep,Somen->matchPtime.of_spanp,Ptime.of_spannwith|None,_|_,None->Lwt.return@@Error(`Not_foundkey)|Somep_ts,Somea_ts->ifPtime.is_latera_ts~than:p_tsthenLwt.return@@Oknew_spanelseLwt.return@@Okprev)(OkPtime.Span.(zero|>to_d_ps))l(* this is probably a surprising implementation for `batch`. Concurrent writes are not
* supported by this implementation (there's a global write mutex) so we don't have
* to do any work to make sure that writes don't get in each other's way. *)letbatcht?(retries=13)f=let_=retriesinft(** [digest t key] is the SHA256 sum of `key` if `key` is a value.
* If [key] is a dictionary, it's a recursive digest of `key`'s contents. *)letdigesttkey=letrecauxctxtkey=gettkey>>=function|Okv->letdigest=Digestif.SHA256.feed_stringctxvinLwt.return@@Okdigest|Error(`Value_expected_)->begin(* let's see whether we can get a digest for the directory contents *)(* unfortunately we can't just run a digest of the block list,
* because CTZs can change file contents without changing
* metadata if the length remains the same, and also because
* there are many differences possible in the entry list that map to the same
* filesystem structure *)listtkey>>=function|Errore->Log.err(funm->m"error listing %a: %a\n%!"Mirage_kv.Key.ppkeypp_errore);Lwt.return@@Error(`Not_foundkey)|Okl->begin(* There's no explicit statement in the mli about whether
* we should descend beyond 1 dictionary for `digest`,
* but I'm not sure how we can meaningfully have a digest if we don't *)Lwt_list.fold_left_s(functx_result(basename,_)->matchctx_resultwith|Error_ase->Lwt.returne|Okctx->letpath=Mirage_kv.Key.addkeybasenameinauxctxtpath)(Okctx)lendend|Error_ase->Lwt.returneinletctx=Digestif.SHA256.init()inLog.debug(funf->f"context for digest initiated");auxctxtkey>|=function|Errore->Errore|Okctx->OkDigestif.SHA256.(to_raw_string@@getctx)letdisconnect_=Lwt.return_unitletconnect~program_block_sizeblock=Sectors.get_infoblock>>=funinfo->letblock_size=info.Mirage_block.sector_sizeinFs.connect~program_block_size~block_sizeblockletformat~program_block_sizeblock=Sectors.get_infoblock>>=funinfo->letblock_size=info.Mirage_block.sector_sizeinFs.format~program_block_size~block_sizeblockend