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 }