Newer
Older
dub_jkp / source / dub / packagemanager.d
  1. /**
  2. Management of packages on the local computer.
  3.  
  4. Copyright: © 2012-2013 rejectedsoftware e.K.
  5. License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
  6. Authors: Sönke Ludwig, Matthias Dondorff
  7. */
  8. module dub.packagemanager;
  9.  
  10. import dub.dependency;
  11. import dub.internal.utils;
  12. import dub.internal.vibecompat.core.file;
  13. import dub.internal.vibecompat.core.log;
  14. import dub.internal.vibecompat.data.json;
  15. import dub.internal.vibecompat.inet.path;
  16. import dub.package_;
  17.  
  18. import std.algorithm : countUntil, filter, sort, canFind;
  19. import std.array;
  20. import std.conv;
  21. import std.digest.sha;
  22. import std.encoding : sanitize;
  23. import std.exception;
  24. import std.file;
  25. import std.string;
  26. import std.zip;
  27.  
  28.  
  29. enum JournalJsonFilename = "journal.json";
  30. enum LocalPackagesFilename = "local-packages.json";
  31.  
  32.  
  33. private struct Repository {
  34. Path path;
  35. Path packagePath;
  36. Path[] searchPath;
  37. Package[] localPackages;
  38.  
  39. this(Path path)
  40. {
  41. this.path = path;
  42. this.packagePath = path ~"packages/";
  43. }
  44. }
  45.  
  46. enum LocalPackageType {
  47. user,
  48. system
  49. }
  50.  
  51. /// The PackageManager can retrieve present packages and get / remove
  52. /// packages.
  53. class PackageManager {
  54. private {
  55. Repository[LocalPackageType] m_repositories;
  56. Path[] m_searchPath;
  57. Package[] m_packages;
  58. Package[] m_temporaryPackages;
  59. }
  60.  
  61. this(Path user_path, Path system_path)
  62. {
  63. m_repositories[LocalPackageType.user] = Repository(user_path);
  64. m_repositories[LocalPackageType.system] = Repository(system_path);
  65. refresh(true);
  66. }
  67.  
  68. @property void searchPath(Path[] paths) { m_searchPath = paths.dup; refresh(false); }
  69. @property const(Path)[] searchPath() const { return m_searchPath; }
  70.  
  71. @property const(Path)[] completeSearchPath()
  72. const {
  73. auto ret = appender!(Path[])();
  74. ret.put(m_searchPath);
  75. ret.put(m_repositories[LocalPackageType.user].searchPath);
  76. ret.put(m_repositories[LocalPackageType.user].packagePath);
  77. ret.put(m_repositories[LocalPackageType.system].searchPath);
  78. ret.put(m_repositories[LocalPackageType.system].packagePath);
  79. return ret.data;
  80. }
  81.  
  82. Package getPackage(string name, Version ver)
  83. {
  84. foreach( p; getPackageIterator(name) )
  85. if( p.ver == ver )
  86. return p;
  87. return null;
  88. }
  89.  
  90. Package getPackage(string name, string ver, Path in_path)
  91. {
  92. return getPackage(name, Version(ver), in_path);
  93. }
  94. Package getPackage(string name, Version ver, Path in_path)
  95. {
  96. foreach( p; getPackageIterator(name) )
  97. if (p.ver == ver && p.path.startsWith(in_path))
  98. return p;
  99. return null;
  100. }
  101.  
  102. Package getPackage(string name, string ver)
  103. {
  104. foreach (ep; getPackageIterator(name)) {
  105. if (ep.vers == ver)
  106. return ep;
  107. }
  108. return null;
  109. }
  110.  
  111. Package getFirstPackage(string name)
  112. {
  113. foreach (ep; getPackageIterator(name))
  114. return ep;
  115. return null;
  116. }
  117.  
  118. Package getPackage(Path path)
  119. {
  120. foreach (p; getPackageIterator())
  121. if (!p.basePackage && p.path == path)
  122. return p;
  123. auto pack = new Package(path);
  124. addPackages(m_temporaryPackages, pack);
  125. return pack;
  126. }
  127.  
  128. Package getBestPackage(string name, string version_spec)
  129. {
  130. return getBestPackage(name, Dependency(version_spec));
  131. }
  132.  
  133. Package getBestPackage(string name, Dependency version_spec)
  134. {
  135. Package ret;
  136. foreach( p; getPackageIterator(name) )
  137. if( version_spec.matches(p.ver) && (!ret || p.ver > ret.ver) )
  138. ret = p;
  139. return ret;
  140. }
  141.  
  142. /** Determines if a package is managed by DUB.
  143.  
  144. Managed packages can be upgraded and removed.
  145. */
  146. bool isManagedPackage(Package pack)
  147. const {
  148. auto ppath = pack.basePackage.path;
  149. foreach (rep; m_repositories) {
  150. auto rpath = rep.packagePath;
  151. if (ppath.startsWith(rpath))
  152. return true;
  153. }
  154. return false;
  155. }
  156.  
  157. int delegate(int delegate(ref Package)) getPackageIterator()
  158. {
  159. int iterator(int delegate(ref Package) del)
  160. {
  161. int handlePackage(Package p) {
  162. if (auto ret = del(p)) return ret;
  163. foreach (sp; p.subPackages)
  164. if (auto ret = del(sp))
  165. return ret;
  166. return 0;
  167. }
  168.  
  169. foreach (tp; m_temporaryPackages)
  170. if (auto ret = handlePackage(tp)) return ret;
  171.  
  172. // first search local packages
  173. foreach (tp; LocalPackageType.min .. LocalPackageType.max+1)
  174. foreach (p; m_repositories[cast(LocalPackageType)tp].localPackages)
  175. if (auto ret = handlePackage(p)) return ret;
  176.  
  177. // and then all packages gathered from the search path
  178. foreach( p; m_packages )
  179. if( auto ret = handlePackage(p) )
  180. return ret;
  181. return 0;
  182. }
  183.  
  184. return &iterator;
  185. }
  186.  
  187. int delegate(int delegate(ref Package)) getPackageIterator(string name)
  188. {
  189. int iterator(int delegate(ref Package) del)
  190. {
  191. foreach (p; getPackageIterator())
  192. if (p.name == name)
  193. if (auto ret = del(p)) return ret;
  194. return 0;
  195. }
  196.  
  197. return &iterator;
  198. }
  199.  
  200. /// Extracts the package supplied as a path to it's zip file to the
  201. /// destination and sets a version field in the package description.
  202. Package storeFetchedPackage(Path zip_file_path, Json package_info, Path destination)
  203. {
  204. auto package_name = package_info.name.get!string();
  205. auto package_version = package_info["version"].get!string();
  206. auto clean_package_version = package_version[package_version.startsWith("~") ? 1 : 0 .. $];
  207.  
  208. logDiagnostic("Placing package '%s' version '%s' to location '%s' from file '%s'",
  209. package_name, package_version, destination.toNativeString(), zip_file_path.toNativeString());
  210.  
  211. if( existsFile(destination) ){
  212. throw new Exception(format("%s (%s) needs to be removed from '%s' prior placement.", package_name, package_version, destination));
  213. }
  214.  
  215. // open zip file
  216. ZipArchive archive;
  217. {
  218. logDebug("Opening file %s", zip_file_path);
  219. auto f = openFile(zip_file_path, FileMode.Read);
  220. scope(exit) f.close();
  221. archive = new ZipArchive(f.readAll());
  222. }
  223.  
  224. logDebug("Extracting from zip.");
  225.  
  226. // In a github zip, the actual contents are in a subfolder
  227. Path zip_prefix;
  228. outer: foreach(ArchiveMember am; archive.directory) {
  229. auto path = Path(am.name);
  230. foreach (fil; packageInfoFilenames)
  231. if (path.length == 2 && path.head.toString == fil) {
  232. zip_prefix = path[0 .. $-1];
  233. break outer;
  234. }
  235. }
  236.  
  237. logDebug("zip root folder: %s", zip_prefix);
  238.  
  239. Path getCleanedPath(string fileName) {
  240. auto path = Path(fileName);
  241. if(zip_prefix != Path() && !path.startsWith(zip_prefix)) return Path();
  242. return path[zip_prefix.length..path.length];
  243. }
  244.  
  245. // extract & place
  246. mkdirRecurse(destination.toNativeString());
  247. auto journal = new Journal;
  248. logDiagnostic("Copying all files...");
  249. int countFiles = 0;
  250. foreach(ArchiveMember a; archive.directory) {
  251. auto cleanedPath = getCleanedPath(a.name);
  252. if(cleanedPath.empty) continue;
  253. auto dst_path = destination~cleanedPath;
  254.  
  255. logDebug("Creating %s", cleanedPath);
  256. if( dst_path.endsWithSlash ){
  257. if( !existsDirectory(dst_path) )
  258. mkdirRecurse(dst_path.toNativeString());
  259. journal.add(Journal.Entry(Journal.Type.Directory, cleanedPath));
  260. } else {
  261. if( !existsDirectory(dst_path.parentPath) )
  262. mkdirRecurse(dst_path.parentPath.toNativeString());
  263. auto dstFile = openFile(dst_path, FileMode.CreateTrunc);
  264. scope(exit) dstFile.close();
  265. dstFile.put(archive.expand(a));
  266. journal.add(Journal.Entry(Journal.Type.RegularFile, cleanedPath));
  267. ++countFiles;
  268. }
  269. }
  270. logDiagnostic("%s file(s) copied.", to!string(countFiles));
  271.  
  272. // overwrite package.json (this one includes a version field)
  273. auto pack = new Package(destination);
  274. pack.info.version_ = package_info["version"].get!string;
  275.  
  276. if (pack.packageInfoFile.head != defaultPackageFilename()) {
  277. // Storeinfo saved a default file, this could be different to the file from the zip.
  278. removeFile(pack.packageInfoFile);
  279. journal.remove(Journal.Entry(Journal.Type.RegularFile, Path(pack.packageInfoFile.head)));
  280. journal.add(Journal.Entry(Journal.Type.RegularFile, Path(defaultPackageFilename())));
  281. }
  282. pack.storeInfo();
  283.  
  284. // Write journal
  285. logDebug("Saving retrieval action journal...");
  286. journal.add(Journal.Entry(Journal.Type.RegularFile, Path(JournalJsonFilename)));
  287. journal.save(destination ~ JournalJsonFilename);
  288.  
  289. addPackages(m_packages, pack);
  290.  
  291. return pack;
  292. }
  293.  
  294. /// Removes the given the package.
  295. void remove(in Package pack)
  296. {
  297. logDebug("Remove %s, version %s, path '%s'", pack.name, pack.vers, pack.path);
  298. enforce(!pack.path.empty, "Cannot remove package "~pack.name~" without a path.");
  299.  
  300. // remove package from repositories' list
  301. bool found = false;
  302. bool removeFrom(Package[] packs, in Package pack) {
  303. auto packPos = countUntil!("a.path == b.path")(packs, pack);
  304. if(packPos != -1) {
  305. packs = std.algorithm.remove(packs, packPos);
  306. return true;
  307. }
  308. return false;
  309. }
  310. foreach(repo; m_repositories) {
  311. if(removeFrom(repo.localPackages, pack)) {
  312. found = true;
  313. break;
  314. }
  315. }
  316. if(!found)
  317. found = removeFrom(m_packages, pack);
  318. enforce(found, "Cannot remove, package not found: '"~ pack.name ~"', path: " ~ to!string(pack.path));
  319.  
  320. // delete package files physically
  321. logDebug("Looking up journal");
  322. auto journalFile = pack.path~JournalJsonFilename;
  323. if (!existsFile(journalFile))
  324. throw new Exception("Removal failed, no retrieval journal found for '"~pack.name~"'. Please remove the folder '%s' manually.", pack.path.toNativeString());
  325.  
  326. auto packagePath = pack.path;
  327. auto journal = new Journal(journalFile);
  328. logDebug("Erasing files");
  329. foreach( Journal.Entry e; filter!((Journal.Entry a) => a.type == Journal.Type.RegularFile)(journal.entries)) {
  330. logDebug("Deleting file '%s'", e.relFilename);
  331. auto absFile = pack.path~e.relFilename;
  332. if(!existsFile(absFile)) {
  333. logWarn("Previously retrieved file not found for removal: '%s'", absFile);
  334. continue;
  335. }
  336.  
  337. removeFile(absFile);
  338. }
  339.  
  340. logDiagnostic("Erasing directories");
  341. Path[] allPaths;
  342. foreach(Journal.Entry e; filter!((Journal.Entry a) => a.type == Journal.Type.Directory)(journal.entries))
  343. allPaths ~= pack.path~e.relFilename;
  344. sort!("a.length>b.length")(allPaths); // sort to erase deepest paths first
  345. foreach(Path p; allPaths) {
  346. logDebug("Deleting folder '%s'", p);
  347. if( !existsFile(p) || !isDir(p.toNativeString()) || !isEmptyDir(p) ) {
  348. logError("Alien files found, directory is not empty or is not a directory: '%s'", p);
  349. continue;
  350. }
  351. rmdir(p.toNativeString());
  352. }
  353.  
  354. // Erase .dub folder, this is completely erased.
  355. auto dubDir = (pack.path ~ ".dub/").toNativeString();
  356. enforce(!existsFile(dubDir) || isDir(dubDir), ".dub should be a directory, but is a file.");
  357. if(existsFile(dubDir) && isDir(dubDir)) {
  358. logDebug(".dub directory found, removing directory including content.");
  359. rmdirRecurse(dubDir);
  360. }
  361.  
  362. logDebug("About to delete root folder for package '%s'.", pack.path);
  363. if(!isEmptyDir(pack.path))
  364. throw new Exception("Alien files found in '"~pack.path.toNativeString()~"', needs to be deleted manually.");
  365.  
  366. rmdir(pack.path.toNativeString());
  367. logInfo("Removed package: '"~pack.name~"'");
  368. }
  369.  
  370. Package addLocalPackage(in Path path, string verName, LocalPackageType type)
  371. {
  372. auto pack = new Package(path);
  373. enforce(pack.name.length, "The package has no name, defined in: " ~ path.toString());
  374. if (verName.length)
  375. pack.ver = Version(verName);
  376.  
  377. // don't double-add packages
  378. Package[]* packs = &m_repositories[type].localPackages;
  379. foreach (p; *packs) {
  380. if (p.path == path) {
  381. enforce(p.ver == pack.ver, "Adding the same local package twice with differing versions is not allowed.");
  382. logInfo("Package is already registered: %s (version: %s)", p.name, p.ver);
  383. return p;
  384. }
  385. }
  386.  
  387. addPackages(*packs, pack);
  388.  
  389. writeLocalPackageList(type);
  390.  
  391. logInfo("Registered package: %s (version: %s)", pack.name, pack.ver);
  392. return pack;
  393. }
  394.  
  395. void removeLocalPackage(in Path path, LocalPackageType type)
  396. {
  397. Package[]* packs = &m_repositories[type].localPackages;
  398. size_t[] to_remove;
  399. foreach( i, entry; *packs )
  400. if( entry.path == path )
  401. to_remove ~= i;
  402. enforce(to_remove.length > 0, "No "~type.to!string()~" package found at "~path.toNativeString());
  403.  
  404. string[Version] removed;
  405. foreach_reverse( i; to_remove ) {
  406. removed[(*packs)[i].ver] = (*packs)[i].name;
  407. *packs = (*packs)[0 .. i] ~ (*packs)[i+1 .. $];
  408. }
  409.  
  410. writeLocalPackageList(type);
  411.  
  412. foreach(ver, name; removed)
  413. logInfo("Unregistered package: %s (version: %s)", name, ver);
  414. }
  415.  
  416. Package getTemporaryPackage(Path path, Version ver)
  417. {
  418. foreach (p; m_temporaryPackages)
  419. if (p.path == path) {
  420. enforce(p.ver == ver, format("Package in %s is refrenced with two conflicting versions: %s vs %s", path.toNativeString(), p.ver, ver));
  421. return p;
  422. }
  423. auto pack = new Package(path);
  424. enforce(pack.name.length, "The package has no name, defined in: " ~ path.toString());
  425. pack.ver = ver;
  426. addPackages(m_temporaryPackages, pack);
  427. return pack;
  428. }
  429.  
  430. /// For the given type add another path where packages will be looked up.
  431. void addSearchPath(Path path, LocalPackageType type)
  432. {
  433. m_repositories[type].searchPath ~= path;
  434. writeLocalPackageList(type);
  435. }
  436.  
  437. /// Removes a search path from the given type.
  438. void removeSearchPath(Path path, LocalPackageType type)
  439. {
  440. m_repositories[type].searchPath = m_repositories[type].searchPath.filter!(p => p != path)().array();
  441. writeLocalPackageList(type);
  442. }
  443.  
  444. void refresh(bool refresh_existing_packages)
  445. {
  446. // load locally defined packages
  447. void scanLocalPackages(LocalPackageType type)
  448. {
  449. Path list_path = m_repositories[type].packagePath;
  450. Package[] packs;
  451. Path[] paths;
  452. try {
  453. auto local_package_file = list_path ~ LocalPackagesFilename;
  454. logDiagnostic("Looking for local package map at %s", local_package_file.toNativeString());
  455. if( !existsFile(local_package_file) ) return;
  456. logDiagnostic("Try to load local package map at %s", local_package_file.toNativeString());
  457. auto packlist = jsonFromFile(list_path ~ LocalPackagesFilename);
  458. enforce(packlist.type == Json.Type.array, LocalPackagesFilename~" must contain an array.");
  459. foreach( pentry; packlist ){
  460. try {
  461. auto name = pentry.name.get!string();
  462. auto path = Path(pentry.path.get!string());
  463. if (name == "*") {
  464. paths ~= path;
  465. } else {
  466. auto ver = Version(pentry["version"].get!string());
  467.  
  468. Package pp;
  469. if (!refresh_existing_packages) {
  470. foreach (p; m_repositories[type].localPackages)
  471. if (p.path == path) {
  472. pp = p;
  473. break;
  474. }
  475. }
  476.  
  477. if (!pp) {
  478. if (Package.isPackageAt(path)) pp = new Package(path);
  479. else {
  480. auto info = Json.emptyObject;
  481. info.name = name;
  482. }
  483. }
  484.  
  485. if (pp.name != name)
  486. logWarn("Local package at %s has different name than %s (%s)", path.toNativeString(), name, pp.name);
  487. pp.ver = ver;
  488.  
  489. addPackages(packs, pp);
  490. }
  491. } catch( Exception e ){
  492. logWarn("Error adding local package: %s", e.msg);
  493. }
  494. }
  495. } catch( Exception e ){
  496. logDiagnostic("Loading of local package list at %s failed: %s", list_path.toNativeString(), e.msg);
  497. }
  498. m_repositories[type].localPackages = packs;
  499. m_repositories[type].searchPath = paths;
  500. }
  501. scanLocalPackages(LocalPackageType.system);
  502. scanLocalPackages(LocalPackageType.user);
  503.  
  504. auto old_packages = m_packages;
  505.  
  506. // rescan the system and user package folder
  507. void scanPackageFolder(Path path)
  508. {
  509. if( path.existsDirectory() ){
  510. logDebug("iterating dir %s", path.toNativeString());
  511. try foreach( pdir; iterateDirectory(path) ){
  512. logDebug("iterating dir %s entry %s", path.toNativeString(), pdir.name);
  513. if( !pdir.isDirectory ) continue;
  514. auto pack_path = path ~ pdir.name;
  515. if (!Package.isPackageAt(pack_path)) continue;
  516. Package p;
  517. try {
  518. if (!refresh_existing_packages)
  519. foreach (pp; old_packages)
  520. if (pp.path == pack_path) {
  521. p = pp;
  522. break;
  523. }
  524. if (!p) p = new Package(pack_path);
  525. addPackages(m_packages, p);
  526. } catch( Exception e ){
  527. logError("Failed to load package in %s: %s", pack_path, e.msg);
  528. logDiagnostic("Full error: %s", e.toString().sanitize());
  529. }
  530. }
  531. catch(Exception e) logDiagnostic("Failed to enumerate %s packages: %s", path.toNativeString(), e.toString());
  532. }
  533. }
  534.  
  535. m_packages = null;
  536. foreach (p; this.completeSearchPath)
  537. scanPackageFolder(p);
  538. }
  539.  
  540. alias ubyte[] Hash;
  541. /// Generates a hash value for a given package.
  542. /// Some files or folders are ignored during the generation (like .dub and
  543. /// .svn folders)
  544. Hash hashPackage(Package pack)
  545. {
  546. string[] ignored_directories = [".git", ".dub", ".svn"];
  547. // something from .dub_ignore or what?
  548. string[] ignored_files = [];
  549. SHA1 sha1;
  550. foreach(file; dirEntries(pack.path.toNativeString(), SpanMode.depth)) {
  551. if(file.isDir && ignored_directories.canFind(Path(file.name).head.toString()))
  552. continue;
  553. else if(ignored_files.canFind(Path(file.name).head.toString()))
  554. continue;
  555.  
  556. sha1.put(cast(ubyte[])Path(file.name).head.toString());
  557. if(file.isDir) {
  558. logDebug("Hashed directory name %s", Path(file.name).head);
  559. }
  560. else {
  561. sha1.put(openFile(Path(file.name)).readAll());
  562. logDebug("Hashed file contents from %s", Path(file.name).head);
  563. }
  564. }
  565. auto hash = sha1.finish();
  566. logDebug("Project hash: %s", hash);
  567. return hash[0..$];
  568. }
  569.  
  570. private void writeLocalPackageList(LocalPackageType type)
  571. {
  572. Json[] newlist;
  573. foreach (p; m_repositories[type].searchPath) {
  574. auto entry = Json.emptyObject;
  575. entry.name = "*";
  576. entry.path = p.toNativeString();
  577. newlist ~= entry;
  578. }
  579.  
  580. foreach (p; m_repositories[type].localPackages) {
  581. auto entry = Json.emptyObject;
  582. entry["name"] = p.name;
  583. entry["version"] = p.ver.toString();
  584. entry["path"] = p.path.toNativeString();
  585. newlist ~= entry;
  586. }
  587.  
  588. Path path = m_repositories[type].packagePath;
  589. if( !existsDirectory(path) ) mkdirRecurse(path.toNativeString());
  590. writeJsonFile(path ~ LocalPackagesFilename, Json(newlist));
  591. }
  592.  
  593. /// Adds the package and scans for subpackages.
  594. private void addPackages(ref Package[] dst_repos, Package pack) const {
  595. // Add the main package.
  596. dst_repos ~= pack;
  597.  
  598. // Additionally to the internally defined subpackages, whose metadata
  599. // is loaded with the main package.json, load all externally defined
  600. // packages after the package is available with all the data.
  601. foreach ( sub_name, sub_path; pack.exportedPackages ) {
  602. auto path = pack.path ~ sub_path;
  603. if ( !existsFile(path) ) {
  604. logError("Package %s defined sub-package %s, definition file is missing: ", sub_name, path.toNativeString());
  605. continue;
  606. }
  607. // Add the subpackage.
  608. try {
  609. auto sub_pack = new Package(path, pack);
  610. // Checking the raw name here, instead of the "parent:sub" style.
  611. enforce(sub_pack.info.name == sub_name, "Name of package '" ~ sub_name ~ "' differs in definition in '" ~ path.toNativeString() ~ "'.");
  612. dst_repos ~= sub_pack;
  613. } catch( Exception e ){
  614. logError("Package '%s': Failed to load sub-package '%s' in %s, error: %s", pack.name, sub_name, path.toNativeString(), e.msg);
  615. logDiagnostic("Full error: %s", e.toString().sanitize());
  616. }
  617. }
  618. }
  619. }
  620.  
  621.  
  622. /**
  623. Retrieval journal for later removal, keeping track of placed files
  624. files.
  625. Example Json:
  626. {
  627. "version": 1,
  628. "files": {
  629. "file1": "typeoffile1",
  630. ...
  631. }
  632. }
  633. */
  634. private class Journal {
  635. private enum Version = 1;
  636. enum Type {
  637. RegularFile,
  638. Directory,
  639. Alien
  640. }
  641. struct Entry {
  642. this( Type t, Path f ) { type = t; relFilename = f; }
  643. Type type;
  644. Path relFilename;
  645. }
  646. @property const(Entry[]) entries() const { return m_entries; }
  647. this() {}
  648. /// Initializes a Journal from a json file.
  649. this(Path journalFile) {
  650. auto jsonJournal = jsonFromFile(journalFile);
  651. enforce(cast(int)jsonJournal["Version"] == Version, "Mismatched version: "~to!string(cast(int)jsonJournal["Version"]) ~ "vs. " ~to!string(Version));
  652. foreach(string file, type; jsonJournal["Files"])
  653. m_entries ~= Entry(to!Type(cast(string)type), Path(file));
  654. }
  655.  
  656. void add(Entry e) {
  657. foreach(Entry ent; entries) {
  658. if( e.relFilename == ent.relFilename ) {
  659. enforce(e.type == ent.type, "Duplicate('"~to!string(e.relFilename)~"'), different types: "~to!string(e.type)~" vs. "~to!string(ent.type));
  660. return;
  661. }
  662. }
  663. m_entries ~= e;
  664. }
  665.  
  666. void remove(Entry e) {
  667. foreach(i, Entry ent; entries) {
  668. if( e.relFilename == ent.relFilename ) {
  669. m_entries = std.algorithm.remove(m_entries, i);
  670. return;
  671. }
  672. }
  673. enforce(false, "Cannot remove entry, not available: " ~ e.relFilename.toNativeString());
  674. }
  675. /// Save the current state to the path.
  676. void save(Path path) {
  677. Json jsonJournal = serialize();
  678. auto fileJournal = openFile(path, FileMode.CreateTrunc);
  679. scope(exit) fileJournal.close();
  680. fileJournal.writePrettyJsonString(jsonJournal);
  681. }
  682. private Json serialize() const {
  683. Json[string] files;
  684. foreach(Entry e; m_entries)
  685. files[to!string(e.relFilename)] = to!string(e.type);
  686. Json[string] json;
  687. json["Version"] = Version;
  688. json["Files"] = files;
  689. return Json(json);
  690. }
  691. private {
  692. Entry[] m_entries;
  693. }
  694. }