1 /** 2 * ae.sys.archive 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <ae@cy.md> 12 */ 13 14 module ae.sys.archive; 15 16 import std.array; 17 import std.conv; 18 import std.datetime; 19 import std.exception; 20 import std.file; 21 import std.path; 22 import std.process; 23 import std.string; 24 25 import ae.sys.file; 26 import ae.sys.install.sevenzip; 27 import ae.utils.meta; 28 import ae.utils.path : haveExecutable; 29 30 /// Unzips a .zip file to the target directory. 31 void unzip(string zip, string target) 32 { 33 import std.zip; 34 auto archive = new ZipArchive(zip.read); 35 foreach (name, entry; archive.directory) 36 { 37 auto path = buildPath(target, name).replace("\\", "/"); 38 ensurePathExists(path); 39 40 auto attr = entry.fileAttributes; 41 42 if (name.endsWith(`/`)) 43 { 44 if (!path.exists) 45 path.mkdirRecurse(); 46 } 47 else 48 { 49 bool isLink = false; 50 version (Posix) 51 { 52 import core.sys.posix.sys.stat; 53 if (S_ISLNK(attr.to!mode_t)) 54 isLink = true; 55 } 56 if (isLink) 57 { 58 symlink(cast(string)archive.expand(entry), path); 59 continue; // Don't try to chmod the link target! 60 } 61 else 62 std.file.write(path, archive.expand(entry)); 63 } 64 65 if (attr) 66 path.setAttributes(attr); 67 68 auto time = entry.time().DosFileTimeToSysTime(UTC()); 69 path.setTimes(time, time); 70 } 71 } 72 73 /// Unpacks a file with 7-Zip to the specified directory, 74 /// installing it locally if necessary. 75 void un7z(string archive, string target) 76 { 77 sevenZip.require(); 78 target.mkdirRecurse(); 79 auto pid = spawnProcess([sevenZip.exe, "x", "-o" ~ target, archive]); 80 enforce(pid.wait() == 0, "Extraction failed"); 81 } 82 83 /// Unpacks an archive to the specified directory. 84 /// Uses std.zip for .zip files, and invokes tar (if available) 85 /// or 7-Zip (installing it locally if necessary) for other file types. 86 /// Always unpacks compressed tar archives in one go. 87 void unpack(string archive, string target) 88 { 89 bool untar(string longExtension, string shortExtension, string tarSwitch, string unpacker) 90 { 91 if ((archive.toLower().endsWith(longExtension) || archive.toLower().endsWith(shortExtension)) && haveExecutable(unpacker)) 92 { 93 target.mkdirRecurse(); 94 auto pid = spawnProcess(["tar", "xf", archive, tarSwitch, "--directory", target]); 95 enforce(pid.wait() == 0, "Extraction failed"); 96 return true; 97 } 98 return false; 99 } 100 101 if (archive.toLower().endsWith(".zip")) 102 archive.unzip(target); 103 else 104 if (haveExecutable("tar") && or( 105 untar(".tar.gz" , ".tgz" , "--gzip" , "gzip" ), 106 untar(".tar.bz2" , ".tbz" , "--bzip2", "bzip2"), 107 untar(".tar.lzma", ".tlz" , "--lzma" , "lzma" ), 108 untar(".tar.xz" , ".txz" , "--xz" , "xz" ), 109 untar(".tar.zst" , ".tzst", "--zstd" , "zstd" ), 110 )) 111 {} 112 else 113 if (archive.extension.toLower == ".rar" && haveExecutable("unrar")) 114 { 115 target.mkdirRecurse(); 116 auto pid = spawnProcess(["unrar", "x", archive, target]); 117 enforce(pid.wait() == 0, "Extraction failed"); 118 } 119 else 120 { 121 auto tar = archive.stripExtension; 122 if (tar.extension.toLower == ".tar") 123 { 124 un7z(archive, archive.dirName); 125 enforce(tar.exists, "Expected to unpack " ~ archive ~ " to " ~ tar); 126 scope(exit) tar.remove(); 127 un7z(tar, target); 128 } 129 else 130 un7z(archive, target); 131 } 132 }