dlangui/src/dlangui/core/files.d

611 lines
20 KiB
D

// Written in the D programming language.
/**
This module contains cross-platform file access utilities
Synopsis:
----
import dlangui.core.files;
----
Copyright: Vadim Lopatin, 2014
License: Boost License 1.0
Authors: Vadim Lopatin, coolreader.org@gmail.com
*/
module dlangui.core.files;
import std.algorithm;
private import dlangui.core.logger;
private import std.process;
private import std.path;
private import std.file;
private import std.utf;
/// path delimiter (\ for windows, / for others)
enum char PATH_DELIMITER = dirSeparator[0];
/// Filesystem root entry / bookmark types
enum RootEntryType : uint {
/// filesystem root
ROOT,
/// current user home
HOME,
/// removable drive
REMOVABLE,
/// fixed drive
FIXED,
/// network
NETWORK,
/// cd rom
CDROM,
/// sd card
SDCARD,
/// custom bookmark
BOOKMARK,
}
/// Filesystem root entry item
struct RootEntry {
private RootEntryType _type;
private string _path;
private dstring _display;
this(RootEntryType type, string path, dstring display = null) {
_type = type;
_path = path;
_display = display;
if (display is null) {
_display = toUTF32(baseName(path));
}
}
/// Returns type
@property RootEntryType type() { return _type; }
/// Returns path
@property string path() { return _path; }
/// Returns display label
@property dstring label() { return _display; }
/// Returns icon resource id
@property string icon() {
switch (type) with(RootEntryType)
{
case NETWORK:
return "folder-network";
case BOOKMARK:
return "folder-bookmark";
case CDROM:
return "drive-optical";
case FIXED:
return "drive-harddisk";
case HOME:
return "user-home";
case ROOT:
return "computer";
case SDCARD:
return "media-flash-sd-mmc";
case REMOVABLE:
return "device-removable-media";
default:
return "folder-blue";
}
}
}
/// Returns user's home directory entry
@property RootEntry homeEntry() {
return RootEntry(RootEntryType.HOME, homePath);
}
/// Returns user's home directory
@property string homePath() {
string path;
version (Windows) {
path = environment.get("USERPROFILE");
if (path is null)
path = environment.get("HOME");
} else {
path = environment.get("HOME");
}
if (path is null)
path = "."; // fallback to current directory
return path;
}
version(OSX) {} else version(Posix)
{
private bool isSpecialFileSystem(const(char)[] dir, const(char)[] type)
{
import std.string : startsWith;
if (dir.startsWith("/dev") || dir.startsWith("/proc") || dir.startsWith("/sys") ||
dir.startsWith("/var/run") || dir.startsWith("/var/lock"))
{
return true;
}
if (type == "tmpfs" || type == "rootfs" || type == "rpc_pipefs") {
return true;
}
return false;
}
private string getDeviceLabelFallback(in char[] type, in char[] fsName)
{
import std.format : format;
import std.string : startsWith;
if (type == "vboxsf") {
return "VirtualBox shared folder";
}
if (fsName.startsWith("gvfsd")) {
return "GNOME virtual file system";
}
return format("%s volume", type);
}
private RootEntryType getDeviceRootEntryType(in char[] type)
{
if (type == "iso9660") {
return RootEntryType.CDROM;
}
if (type == "vfat") {
return RootEntryType.REMOVABLE;
}
return RootEntryType.FIXED;
}
}
version(FreeBSD)
{
private:
import core.sys.posix.sys.types;
enum MFSNAMELEN = 16; /* length of type name including null */
enum MNAMELEN = 88; /* size of on/from name bufs */
enum STATFS_VERSION = 0x20030518; /* current version number */
struct fsid_t
{
int[2] val;
}
struct statfs {
uint f_version; /* structure version number */
uint f_type; /* type of filesystem */
ulong f_flags; /* copy of mount exported flags */
ulong f_bsize; /* filesystem fragment size */
ulong f_iosize; /* optimal transfer block size */
ulong f_blocks; /* total data blocks in filesystem */
ulong f_bfree; /* free blocks in filesystem */
long f_bavail; /* free blocks avail to non-superuser */
ulong f_files; /* total file nodes in filesystem */
long f_ffree; /* free nodes avail to non-superuser */
ulong f_syncwrites; /* count of sync writes since mount */
ulong f_asyncwrites; /* count of async writes since mount */
ulong f_syncreads; /* count of sync reads since mount */
ulong f_asyncreads; /* count of async reads since mount */
ulong[10] f_spare; /* unused spare */
uint f_namemax; /* maximum filename length */
uid_t f_owner; /* user that mounted the filesystem */
fsid_t f_fsid; /* filesystem id */
char[80] f_charspare; /* spare string space */
char[MFSNAMELEN] f_fstypename; /* filesystem type name */
char[MNAMELEN] f_mntfromname; /* mounted filesystem */
char[MNAMELEN] f_mntonname; /* directory on which mounted */
};
extern(C) @nogc nothrow
{
int getmntinfo(statfs **mntbufp, int flags);
}
}
version(linux)
{
private:
import core.stdc.stdio : FILE;
struct mntent
{
char *mnt_fsname; /* Device or server for filesystem. */
char *mnt_dir; /* Directory mounted on. */
char *mnt_type; /* Type of filesystem: ufs, nfs, etc. */
char *mnt_opts; /* Comma-separated options for fs. */
int mnt_freq; /* Dump frequency (in days). */
int mnt_passno; /* Pass number for `fsck'. */
};
extern(C) @nogc nothrow
{
FILE *setmntent(const char *file, const char *mode);
mntent *getmntent(FILE *stream);
mntent *getmntent_r(FILE * stream, mntent *result, char * buffer, int bufsize);
int addmntent(FILE* stream, const mntent *mnt);
int endmntent(FILE * stream);
char *hasmntopt(const mntent *mnt, const char *opt);
}
string unescapeLabel(string label)
{
import std.string : replace;
return label.replace("\\x20", " ")
.replace("\\x9", " ") //actually tab
.replace("\\x5c", "\\")
.replace("\\xA", " "); //actually newline
}
}
/// returns array of system root entries
@property RootEntry[] getRootPaths() {
RootEntry[] res;
res ~= RootEntry(RootEntryType.HOME, homePath);
version (Posix) {
res ~= RootEntry(RootEntryType.ROOT, "/", "File System"d);
}
version(linux) {
import std.string : fromStringz;
mntent ent;
char[1024] buf;
FILE* f = setmntent("/etc/mtab", "r");
if (f) {
scope(exit) endmntent(f);
while(getmntent_r(f, &ent, buf.ptr, cast(int)buf.length) !is null) {
auto fsName = fromStringz(ent.mnt_fsname);
auto mountDir = fromStringz(ent.mnt_dir);
auto type = fromStringz(ent.mnt_type);
if (mountDir == "/" || //root is already added
isSpecialFileSystem(mountDir, type)) //don't list special file systems
{
continue;
}
string label;
enum byLabel = "/dev/disk/by-label";
try {
foreach(entry; dirEntries(byLabel, SpanMode.shallow))
{
if (entry.isSymlink) {
auto normalized = buildNormalizedPath(byLabel, entry.readLink);
if (normalized == fsName) {
label = entry.name.baseName.unescapeLabel();
}
}
}
} catch(Exception e) {
}
if (!label.length) {
label = getDeviceLabelFallback(type, fsName);
}
auto entryType = getDeviceRootEntryType(type);
res ~= RootEntry(entryType, mountDir.idup, label.toUTF32);
}
}
}
version(FreeBSD) {
import std.string : fromStringz;
statfs* mntbufsPtr;
int mntbufsLen = getmntinfo(&mntbufsPtr, 0);
if (mntbufsLen) {
auto mntbufs = mntbufsPtr[0..mntbufsLen];
foreach(buf; mntbufs) {
auto type = fromStringz(buf.f_fstypename.ptr);
auto fsName = fromStringz(buf.f_mntfromname.ptr);
auto mountDir = fromStringz(buf.f_mntonname.ptr);
if (mountDir == "/" || isSpecialFileSystem(mountDir, type)) {
continue;
}
string label = getDeviceLabelFallback(type, fsName);
res ~= RootEntry(getDeviceRootEntryType(type), mountDir.idup, label.toUTF32);
}
}
}
version (Windows) {
import win32.windows;
uint mask = GetLogicalDrives();
foreach(int i; 0 .. 26) {
if (mask & (1 << i)) {
char letter = cast(char)('A' + i);
string path = "" ~ letter ~ ":\\";
dstring display = ""d ~ letter ~ ":"d;
// detect drive type
RootEntryType type;
uint wtype = GetDriveTypeA(("" ~ path).ptr);
//Log.d("Drive ", path, " type ", wtype);
switch (wtype) {
case DRIVE_REMOVABLE:
type = RootEntryType.REMOVABLE;
break;
case DRIVE_REMOTE:
type = RootEntryType.NETWORK;
break;
case DRIVE_CDROM:
type = RootEntryType.CDROM;
break;
default:
type = RootEntryType.FIXED;
break;
}
res ~= RootEntry(type, path, display);
}
}
}
return res;
}
/// returns array of user bookmarked directories
RootEntry[] getBookmarkPaths() nothrow
{
RootEntry[] res;
version(OSX) {
} else version(Android) {
} else version(Posix) {
/*
* Probably we should follow https://www.freedesktop.org/wiki/Specifications/desktop-bookmark-spec/ but it requires XML library.
* So for now just try to read GTK3 bookmarks. Should be compatible with GTK file dialogs, Nautilus and other GTK file managers.
*/
import std.string : startsWith;
import std.stdio : File;
import std.exception : collectException;
try {
enum fileProtocol = "file://";
auto configPath = environment.get("XDG_CONFIG_HOME");
if (!configPath.length) {
configPath = buildPath(homePath(), ".config");
}
auto bookmarksFile = buildPath(configPath, "gtk-3.0/bookmarks");
foreach(line; File(bookmarksFile, "r").byLineCopy()) {
if (line.startsWith(fileProtocol)) {
auto path = line[fileProtocol.length..$];
if (path.isAbsolute) {
// Note: GTK supports regular files in bookmarks too, but we allow directories only.
bool dirExists;
collectException(path.isDir, dirExists);
if (dirExists) {
res ~= RootEntry(RootEntryType.BOOKMARK, path, path.baseName.toUTF32);
}
}
}
}
} catch(Exception e) {
}
} else version(Windows) {
}
return res;
}
/// returns true if directory is root directory (e.g. / or C:\)
bool isRoot(in string path) pure nothrow {
string root = rootName(path);
if (path.equal(root))
return true;
return false;
}
/// returns parent directory for specified path
string parentDir(in string path) pure nothrow {
return buildNormalizedPath(path, "..");
}
/// check filename with pattern
bool filterFilename(in string filename, in string pattern) pure nothrow {
return globMatch(filename.baseName, pattern);
}
/// Filters file name by pattern list
bool filterFilename(in string filename, in string[] filters) pure nothrow {
if (filters.length == 0)
return true; // no filters - show all
foreach(pattern; filters) {
if (filterFilename(filename, pattern))
return true;
}
return false;
}
/** List directory content
Optionally filters file names by filter.
Result will be placed into entries array.
Returns true if directory exists and listed successfully, false otherwise.
*/
bool listDirectory(in string dir, in bool includeDirs, in bool includeFiles, in bool showHiddenFiles, in string[] filters, ref DirEntry[] entries, in bool showExecutables = false) {
entries.length = 0;
import std.exception : collectException;
bool dirExists;
collectException(dir.isDir, dirExists);
if (!dirExists) {
return false;
}
if (!isRoot(dir) && includeDirs) {
entries ~= DirEntry(appendPath(dir, ".."));
}
try {
DirEntry[] dirs;
DirEntry[] files;
foreach (DirEntry e; dirEntries(dir, SpanMode.shallow)) {
string fn = baseName(e.name);
if (!showHiddenFiles && fn.startsWith("."))
continue;
if (e.isDir) {
dirs ~= e;
} else if (e.isFile) {
files ~= e;
}
}
dirs.sort!((a,b) => filenameCmp!(std.path.CaseSensitive.no)(a,b) < 0);
files.sort!((a,b) => filenameCmp!(std.path.CaseSensitive.no)(a,b) < 0);
if (includeDirs)
foreach(DirEntry e; dirs)
entries ~= e;
if (includeFiles)
foreach(DirEntry e; files) {
bool passed = false;
if (showExecutables) {
uint attr_mask = (1 << 0) | (1 << 3) | (1 << 6);
version(Windows) {
passed = e.name.endsWith(".exe") || e.name.endsWith(".EXE")
|| e.name.endsWith(".cmd") || e.name.endsWith(".CMD")
|| e.name.endsWith(".bat") || e.name.endsWith(".BAT");
} else version (Posix) {
// execute permission for others
passed = (e.attributes & attr_mask) != 0;
} else version(OSX) {
passed = (e.attributes & attr_mask) != 0;
}
} else {
passed = filterFilename(e.name, filters);
}
if (passed)
entries ~= e;
}
return true;
} catch (FileException e) {
return false;
}
}
/// Returns true if char ch is / or \ slash
bool isPathDelimiter(in char ch) pure nothrow {
return ch == '/' || ch == '\\';
}
/// Returns current directory
alias currentDir = std.file.getcwd;
/// Returns current executable path only, including last path delimiter - removes executable name from result of std.file.thisExePath()
@property string exePath() {
string path = thisExePath();
int lastSlash = 0;
for (int i = cast(int)path.length - 1; i >= 0; i--)
if (path[i] == PATH_DELIMITER) {
lastSlash = i;
break;
}
return path[0 .. lastSlash + 1];
}
/**
Returns application data directory
On unix, it will return path to subdirectory in home directory - e.g. /home/user/.subdir if ".subdir" is passed as a paramter.
On windows, it will return path to subdir in APPDATA directory - e.g. C:\Users\User\AppData\Roaming\.subdir.
*/
string appDataPath(string subdir = null) {
string path;
version (Windows) {
path = environment.get("APPDATA");
}
if (path is null)
path = homePath;
if (subdir !is null) {
path ~= PATH_DELIMITER;
path ~= subdir;
}
return path;
}
/// Converts path delimiters to standard for platform inplace in buffer(e.g. / to \ on windows, \ to / on posix), returns buf
char[] convertPathDelimiters(char[] buf) {
foreach(ref ch; buf) {
version (Windows) {
if (ch == '/')
ch = '\\';
} else {
if (ch == '\\')
ch = '/';
}
}
return buf;
}
/// Converts path delimiters to standard for platform (e.g. / to \ on windows, \ to / on posix)
string convertPathDelimiters(in string src) {
char[] buf = src.dup;
return cast(string)convertPathDelimiters(buf);
}
/// Appends file path parts with proper delimiters e.g. appendPath("/home/user", ".myapp", "config") => "/home/user/.myapp/config"
string appendPath(string[] pathItems ...) {
char[] buf;
foreach (s; pathItems) {
if (buf.length && !isPathDelimiter(buf[$-1]))
buf ~= PATH_DELIMITER;
buf ~= s;
}
return convertPathDelimiters(buf).dup;
}
/// Appends file path parts with proper delimiters (as well converts delimiters inside path to system) to buffer e.g. appendPath("/home/user", ".myapp", "config") => "/home/user/.myapp/config"
char[] appendPath(char[] buf, string[] pathItems ...) {
foreach (s; pathItems) {
if (buf.length && !isPathDelimiter(buf[$-1]))
buf ~= PATH_DELIMITER;
buf ~= s;
}
return convertPathDelimiters(buf);
}
/** Deprecated: use std.path.pathSplitter instead.
Splits path into elements, e.g. /home/user/dir1 -> ["home", "user", "dir1"], "c:\dir1\dir2" -> ["c:", "dir1", "dir2"]
*/
deprecated string[] splitPath(string path) {
string[] res;
int start = 0;
for (int i = 0; i <= path.length; i++) {
char ch = i < path.length ? path[i] : 0;
if (ch == '\\' || ch == '/' || ch == 0) {
if (start < i)
res ~= path[start .. i].dup;
start = i + 1;
}
}
return res;
}
/// for executable name w/o path, find absolute path to executable
string findExecutablePath(string executableName) {
import std.string : split;
version (Windows) {
if (!executableName.endsWith(".exe"))
executableName = executableName ~ ".exe";
}
string currentExeDir = dirName(thisExePath());
string inCurrentExeDir = absolutePath(buildNormalizedPath(currentExeDir, executableName));
if (exists(inCurrentExeDir) && isFile(inCurrentExeDir))
return inCurrentExeDir; // found in current directory
string pathVariable = environment.get("PATH");
if (!pathVariable)
return null;
string[] paths = pathVariable.split(pathSeparator);
foreach(path; paths) {
string pathname = absolutePath(buildNormalizedPath(path, executableName));
if (exists(pathname) && isFile(pathname))
return pathname;
}
return null;
}