1 /**
2 	File handling.
3 
4 	Copyright: © 2012 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
7 */
8 module dub.internal.vibecompat.core.file;
9 
10 public import dub.internal.vibecompat.inet.url;
11 
12 import dub.internal.logging;
13 
14 import std.conv;
15 import core.stdc.stdio;
16 import std.datetime;
17 import std.exception;
18 import std.file;
19 import std.path;
20 import std.stdio;
21 import std.string;
22 import std.utf;
23 
24 
25 /* Add output range support to File
26 */
27 struct RangeFile {
28 @safe:
29 	std.stdio.File file;
30 
31 	void put(scope const ubyte[] bytes) @trusted { file.rawWrite(bytes); }
32 	void put(scope const char[] str) { put(cast(const(ubyte)[])str); }
33 	void put(char ch) @trusted { put((&ch)[0 .. 1]); }
34 	void put(dchar ch) { char[4] chars; put(chars[0 .. encode(chars, ch)]); }
35 
36 	ubyte[] readAll()
37 	{
38 		auto sz = this.size;
39 		enforce(sz <= size_t.max, "File is too big to read to memory.");
40 		() @trusted { file.seek(0, SEEK_SET); } ();
41 		auto ret = new ubyte[cast(size_t)sz];
42 		rawRead(ret);
43 		return ret;
44 	}
45 
46 	void rawRead(ubyte[] dst) @trusted { enforce(file.rawRead(dst).length == dst.length, "Failed to readall bytes from file."); }
47 	void write(string str) { put(str); }
48 	void close() @trusted { file.close(); }
49 	void flush() @trusted { file.flush(); }
50 	@property ulong size() @trusted { return file.size; }
51 }
52 
53 
54 /**
55 	Opens a file stream with the specified mode.
56 */
57 RangeFile openFile(NativePath path, FileMode mode = FileMode.read)
58 {
59 	string fmode;
60 	final switch(mode){
61 		case FileMode.read: fmode = "rb"; break;
62 		case FileMode.readWrite: fmode = "r+b"; break;
63 		case FileMode.createTrunc: fmode = "wb"; break;
64 		case FileMode.append: fmode = "ab"; break;
65 	}
66 	auto ret = std.stdio.File(path.toNativeString(), fmode);
67 	assert(ret.isOpen);
68 	return RangeFile(ret);
69 }
70 /// ditto
71 RangeFile openFile(string path, FileMode mode = FileMode.read)
72 {
73 	return openFile(NativePath(path), mode);
74 }
75 
76 
77 /**
78 	Moves or renames a file.
79 */
80 void moveFile(NativePath from, NativePath to)
81 {
82 	moveFile(from.toNativeString(), to.toNativeString());
83 }
84 /// ditto
85 void moveFile(string from, string to)
86 {
87 	std.file.rename(from, to);
88 }
89 
90 /**
91 	Copies a file.
92 
93 	Note that attributes and time stamps are currently not retained.
94 
95 	Params:
96 		from = NativePath of the source file
97 		to = NativePath for the destination file
98 		overwrite = If true, any file existing at the destination path will be
99 			overwritten. If this is false, an excpetion will be thrown should
100 			a file already exist at the destination path.
101 
102 	Throws:
103 		An Exception if the copy operation fails for some reason.
104 */
105 void copyFile(NativePath from, NativePath to, bool overwrite = false)
106 {
107 	enforce(existsFile(from), "Source file does not exist.");
108 
109 	if (existsFile(to)) {
110 		enforce(overwrite, "Destination file already exists.");
111 		// remove file before copy to allow "overwriting" files that are in
112 		// use on Linux
113 		removeFile(to);
114 	}
115 
116 	static if (is(PreserveAttributes))
117 	{
118 		.copy(from.toNativeString(), to.toNativeString(), PreserveAttributes.yes);
119 	}
120 	else
121 	{
122 		.copy(from.toNativeString(), to.toNativeString());
123 		// try to preserve ownership/permissions in Posix
124 		version (Posix) {
125 			import core.sys.posix.sys.stat;
126 			import core.sys.posix.unistd;
127 			import std.utf;
128 			auto cspath = toUTFz!(const(char)*)(from.toNativeString());
129 			auto cdpath = toUTFz!(const(char)*)(to.toNativeString());
130 			stat_t st;
131 			enforce(stat(cspath, &st) == 0, "Failed to get attributes of source file.");
132 			if (chown(cdpath, st.st_uid, st.st_gid) != 0)
133 				st.st_mode &= ~(S_ISUID | S_ISGID);
134 			chmod(cdpath, st.st_mode);
135 		}
136 	}
137 }
138 /// ditto
139 void copyFile(string from, string to)
140 {
141 	copyFile(NativePath(from), NativePath(to));
142 }
143 
144 version (Windows) extern(Windows) int CreateHardLinkW(in wchar* to, in wchar* from, void* attr=null);
145 
146 // guess whether 2 files are identical, ignores filename and content
147 private bool sameFile(NativePath a, NativePath b)
148 {
149 	version (Posix) {
150 		auto st_a = std.file.DirEntry(a.toNativeString).statBuf;
151 		auto st_b = std.file.DirEntry(b.toNativeString).statBuf;
152 		return st_a == st_b;
153 	} else {
154 		static assert(__traits(allMembers, FileInfo)[0] == "name");
155 		return getFileInfo(a).tupleof[1 .. $] == getFileInfo(b).tupleof[1 .. $];
156 	}
157 }
158 
159 private bool isWritable(NativePath name)
160 {
161 	version (Windows)
162 	{
163 		import core.sys.windows.windows;
164 
165 		return (name.toNativeString.getAttributes & FILE_ATTRIBUTE_READONLY) == 0;
166 	}
167 	else version (Posix)
168 	{
169 		import core.sys.posix.sys.stat;
170 
171 		return (name.toNativeString.getAttributes & S_IWUSR) != 0;
172 	}
173 	else
174 		static assert(false, "Needs implementation.");
175 }
176 
177 private void makeWritable(NativePath name)
178 {
179 	makeWritable(name.toNativeString);
180 }
181 
182 private void makeWritable(string name)
183 {
184 	version (Windows)
185 	{
186 		import core.sys.windows.windows;
187 
188 		name.setAttributes(name.getAttributes & ~FILE_ATTRIBUTE_READONLY);
189 	}
190 	else version (Posix)
191 	{
192 		import core.sys.posix.sys.stat;
193 
194 		name.setAttributes(name.getAttributes | S_IWUSR);
195 	}
196 	else
197 		static assert(false, "Needs implementation.");
198 }
199 
200 /**
201 	Creates a hardlink if possible, a copy otherwise.
202 
203 	If `from` is read-only and `overwrite` is true, then a copy is made instead
204 	and `to` is made writable; so that repeating the command will not fail.
205 */
206 void hardLinkFile(NativePath from, NativePath to, bool overwrite = false)
207 {
208 	if (existsFile(to)) {
209 		enforce(overwrite, "Destination file already exists.");
210 		if (auto fe = collectException!FileException(removeFile(to))) {
211 			if (sameFile(from, to)) return;
212 			throw fe;
213 		}
214 	}
215 	const writeAccessChangeRequired = overwrite && !isWritable(from);
216 	if (!writeAccessChangeRequired)
217 	{
218 		version (Windows)
219 		{
220 			alias cstr = toUTFz!(const(wchar)*);
221 			if (CreateHardLinkW(cstr(to.toNativeString), cstr(from.toNativeString)))
222 				return;
223 		}
224 		else
225 		{
226 			import core.sys.posix.unistd : link;
227 			alias cstr = toUTFz!(const(char)*);
228 			if (!link(cstr(from.toNativeString), cstr(to.toNativeString)))
229 				return;
230 		}
231 	}
232 	// fallback to copy
233 	copyFile(from, to, overwrite);
234 	if (writeAccessChangeRequired)
235 		to.makeWritable;
236 }
237 
238 /**
239 	Removes a file
240 */
241 void removeFile(NativePath path)
242 {
243 	removeFile(path.toNativeString());
244 }
245 /// ditto
246 void removeFile(string path) {
247 	std.file.remove(path);
248 }
249 
250 /**
251 	Checks if a file exists
252 */
253 bool existsFile(NativePath path) {
254 	return existsFile(path.toNativeString());
255 }
256 /// ditto
257 bool existsFile(string path)
258 {
259 	return std.file.exists(path);
260 }
261 
262 /** Stores information about the specified file/directory into 'info'
263 
264 	Returns false if the file does not exist.
265 */
266 FileInfo getFileInfo(NativePath path)
267 {
268 	auto ent = std.file.DirEntry(path.toNativeString());
269 	return makeFileInfo(ent);
270 }
271 /// ditto
272 FileInfo getFileInfo(string path)
273 {
274 	return getFileInfo(NativePath(path));
275 }
276 
277 /**
278 	Creates a new directory.
279 */
280 void createDirectory(NativePath path)
281 {
282 	mkdir(path.toNativeString());
283 }
284 /// ditto
285 void createDirectory(string path)
286 {
287 	createDirectory(NativePath(path));
288 }
289 
290 /**
291 	Enumerates all files in the specified directory.
292 */
293 void listDirectory(NativePath path, scope bool delegate(FileInfo info) del)
294 {
295 	foreach( DirEntry ent; dirEntries(path.toNativeString(), SpanMode.shallow) )
296 		if( !del(makeFileInfo(ent)) )
297 			break;
298 }
299 /// ditto
300 void listDirectory(string path, scope bool delegate(FileInfo info) del)
301 {
302 	listDirectory(NativePath(path), del);
303 }
304 /// ditto
305 int delegate(scope int delegate(ref FileInfo)) iterateDirectory(NativePath path)
306 {
307 	int iterator(scope int delegate(ref FileInfo) del){
308 		int ret = 0;
309 		listDirectory(path, (fi){
310 			ret = del(fi);
311 			return ret == 0;
312 		});
313 		return ret;
314 	}
315 	return &iterator;
316 }
317 /// ditto
318 int delegate(scope int delegate(ref FileInfo)) iterateDirectory(string path)
319 {
320 	return iterateDirectory(NativePath(path));
321 }
322 
323 
324 /**
325 	Returns the current working directory.
326 */
327 NativePath getWorkingDirectory()
328 {
329 	return NativePath(std.file.getcwd());
330 }
331 
332 
333 /** Contains general information about a file.
334 */
335 struct FileInfo {
336 	/// Name of the file (not including the path)
337 	string name;
338 
339 	/// Size of the file (zero for directories)
340 	ulong size;
341 
342 	/// Time of the last modification
343 	SysTime timeModified;
344 
345 	/// Time of creation (not available on all operating systems/file systems)
346 	SysTime timeCreated;
347 
348 	/// True if this is a symlink to an actual file
349 	bool isSymlink;
350 
351 	/// True if this is a directory or a symlink pointing to a directory
352 	bool isDirectory;
353 }
354 
355 /**
356 	Specifies how a file is manipulated on disk.
357 */
358 enum FileMode {
359 	/// The file is opened read-only.
360 	read,
361 	/// The file is opened for read-write random access.
362 	readWrite,
363 	/// The file is truncated if it exists and created otherwise and the opened for read-write access.
364 	createTrunc,
365 	/// The file is opened for appending data to it and created if it does not exist.
366 	append
367 }
368 
369 /**
370 	Accesses the contents of a file as a stream.
371 */
372 
373 private FileInfo makeFileInfo(DirEntry ent)
374 {
375 	FileInfo ret;
376 	ret.name = baseName(ent.name);
377 	if( ret.name.length == 0 ) ret.name = ent.name;
378 	assert(ret.name.length > 0);
379 	ret.isSymlink = ent.isSymlink;
380 	try {
381 		ret.isDirectory = ent.isDir;
382 		ret.size = ent.size;
383 		ret.timeModified = ent.timeLastModified;
384 		version(Windows) ret.timeCreated = ent.timeCreated;
385 		else ret.timeCreated = ent.timeLastModified;
386 	} catch (Exception e) {
387 		logDiagnostic("Failed to get extended file information for %s: %s", ret.name, e.msg);
388 	}
389 	return ret;
390 }