first commit

This commit is contained in:
skilion 2015-09-01 20:45:34 +02:00
commit 00d53f648e
11 changed files with 1009 additions and 0 deletions

14
Makefile Normal file
View file

@ -0,0 +1,14 @@
DC = dmd
DFLAGS = -unittest -debug -g -od./bin -of./bin/$@ -L-lcurl -L-lsqlite3
SOURCES = \
src/cache.d \
src/config.d \
src/main.d \
src/onedrive.d \
src/sqlite.d \
src/sync.d \
src/util.d
onedrive: $(SOURCES)
$(DC) $(DFLAGS) $(SOURCES)

14
Makefile.win Normal file
View file

@ -0,0 +1,14 @@
DC = dmd
DFLAGS = -debug -g -od./bin -of./bin/$@ lib/sqlite3.lib
SOURCES = \
src/cache.d \
src/config.d \
src/main.d \
src/onedrive.d \
src/sqlite.d \
src/sync.d \
src/util.d
onedrive: $(SOURCES)
$(DC) $(DFLAGS) $(SOURCES)

BIN
lib/sqlite3.lib Normal file

Binary file not shown.

3
onedrive.conf Normal file
View file

@ -0,0 +1,3 @@
client_id = "000000004C15842F"
client_secret = "5vWj5xi6rYZM61X81Z9OyXAmjGhVS6Py"
sync_dir = "~/OneDrive/"

163
src/cache.d Normal file
View file

@ -0,0 +1,163 @@
module cache;
import std.datetime: SysTime, time_t;
import sqlite;
enum ItemType
{
file,
dir
}
struct Item
{
string id;
string path;
string name;
ItemType type;
string eTag;
string cTag;
SysTime mtime;
string parentId;
string crc32;
}
struct ItemCache
{
Database db;
Statement insertItemStmt;
Statement selectItemByIdStmt;
Statement selectItemByPathStmt;
void init()
{
db = Database("cache.db");
db.exec("CREATE TABLE IF NOT EXISTS item (
id TEXT PRIMARY KEY,
path TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
type TEXT NOT NULL,
eTag TEXT NOT NULL,
cTag TEXT NOT NULL,
mtime TEXT NOT NULL,
parentId TEXT NOT NULL,
crc32 TEXT
)");
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS path_idx ON item (path)");
insertItemStmt = db.prepare("INSERT OR REPLACE INTO item (id, path, name, type, eTag, cTag, mtime, parentId, crc32) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
selectItemByIdStmt = db.prepare("SELECT id, path, name, type, eTag, cTag, mtime, parentId, crc32 FROM item WHERE id = ?");
selectItemByPathStmt = db.prepare("SELECT id, path, name, type, eTag, cTag, mtime, parentId, crc32 FROM item WHERE path = ?");
}
void insert(const(char)[] id, const(char)[] name, ItemType type, const(char)[] eTag, const(char)[] cTag, const(char)[] mtime, const(char)[] parentId, const(char)[] crc32)
{
with (insertItemStmt) {
bind(1, id);
bind(2, computePath(name, parentId));
bind(3, name);
string typeStr = void;
final switch (type) {
case ItemType.file: typeStr = "file"; break;
case ItemType.dir: typeStr = "dir"; break;
}
bind(4, typeStr);
bind(5, eTag);
bind(6, cTag);
bind(7, mtime);
bind(8, parentId);
bind(9, crc32);
exec();
}
}
bool selectById(const(char)[] id, out Item item)
{
selectItemByIdStmt.bind(1, id);
auto r = selectItemByIdStmt.exec();
if (!r.empty) {
item = buildItem(r);
return true;
}
return false;
}
bool selectByPath(const(char)[] path, out Item item)
{
selectItemByPathStmt.bind(1, path);
auto r = selectItemByPathStmt.exec();
if (!r.empty) {
item = buildItem(r);
return true;
}
return false;
}
void updateModifiedTime(const(char)[] id, const(char)[] mtime)
{
auto s = db.prepare("UPDATE mtime FROM item WHERE id = ?");
s.bind(1, id);
s.exec();
}
void deleteById(const(char)[] id)
{
auto s = db.prepare("DELETE FROM item WHERE id = ?");
s.bind(1, id);
s.exec();
}
// returns true if the item has the specified parent
bool hasParent(T)(const(char)[] itemId, T parentId)
if (is(T : const(char)[]) || is(T : const(char[])[]))
{
auto s = db.prepare("SELECT parentId FROM item WHERE id = ?");
while (true) {
s.bind(1, itemId);
auto r = s.exec();
if (r.empty) break;
auto currParentId = r.front[0];
static if (is(T : const(char)[])) {
if (currParentId == parentId) return true;
} else {
foreach (id; parentId) if (currParentId == id) return true;
}
itemId = currParentId.dup;
}
return false;
}
private Item buildItem(Statement.Result result)
{
assert(!result.empty && result.front.length == 9);
Item item = {
id: result.front[0].dup,
path: result.front[1].dup,
name: result.front[2].dup,
eTag: result.front[4].dup,
cTag: result.front[5].dup,
mtime: SysTime.fromISOExtString(result.front[6]),
parentId: result.front[7].dup,
crc32: result.front[8].dup
};
switch (result.front[3]) {
case "file": item.type = ItemType.file; break;
case "dir": item.type = ItemType.dir; break;
default: assert(0);
}
return item;
}
private string computePath(const(char)[] name, const(char)[] parentId)
{
auto s = db.prepare("SELECT name, parentId FROM item WHERE id = ?");
string path = name.dup;
while (true) {
s.bind(1, parentId);
auto r = s.exec();
if (r.empty) break;
path = r.front[0].idup ~ "/" ~ path;
parentId = r.front[1].dup;
}
return path;
}
}

64
src/config.d Normal file
View file

@ -0,0 +1,64 @@
import std.regex, std.stdio, std.file;
final class Config
{
private string filename;
private string[string] values;
this(string filename)
{
this.filename = filename;
load();
}
string get(string key)
{
return values[key];
}
void set(string key, string value)
{
values[key] = value;
}
void load()
{
values = null;
scope (failure) return;
auto file = File(filename, "r");
auto r = regex("(?:^\\s*)(\\w+)(?:\\s*=\\s*\")(.*)(?:\"\\s*$)");
foreach (line; file.byLine()) {
auto c = matchFirst(line, r);
if (!c.empty) {
c.popFront(); // skip whole match
string key = c.front.dup;
c.popFront();
values[key] = c.front.dup;
}
}
}
void save()
{
if (exists(filename)) {
string bkpFilename = filename ~ "~";
rename(filename, bkpFilename);
}
auto file = File(filename, "w");
foreach (key, value; values) {
file.writeln(key, " = \"", value, "\"");
}
}
}
unittest
{
auto cfg = new Config("/tmp/test.conf");
cfg.set("test1", "1");
cfg.set("test2", "2");
cfg.set("test1", "3");
cfg.save();
cfg.load();
assert(cfg.get("test1") == "3");
assert(cfg.get("test2") == "2");
}

30
src/main.d Normal file
View file

@ -0,0 +1,30 @@
import std.file;
import config, onedrive, sync;
private string configFile = "./onedrive.conf";
private string refreshTokenFile = "refresh_token";
void main()
{
auto cfg = new Config(configFile);
auto onedrive = new OneDriveApi(cfg.get("client_id"), cfg.get("client_secret"));
try {
string refreshToken = readText(refreshTokenFile);
onedrive.setRefreshToken(refreshToken);
} catch (FileException e) {
onedrive.authorize();
}
onedrive.onRefreshToken = (string refreshToken) { std.file.write(refreshTokenFile, refreshToken); };
auto sync = new SyncEngine(cfg, onedrive);
sync.applyDifferences();
/*import std.stdio;
import std.net.curl;
try {
onedrive.simpleUpload("a.txt", "a.txt", "error").toPrettyString.writeln;
} catch (CurlException e) {
writeln("exc ", e.msg);
}*/
}

219
src/onedrive.d Normal file
View file

@ -0,0 +1,219 @@
module onedrive;
import std.json, std.net.curl, std.path, std.string, std.uni, std.uri;
extern(C) void signal(int sig, void function(int));
private immutable {
string authUrl = "https://login.live.com/oauth20_authorize.srf";
string redirectUrl = "https://login.live.com/oauth20_desktop.srf";
string tokenUrl = "https://login.live.com/oauth20_token.srf";
string itemByIdUrl = "https://api.onedrive.com/v1.0/drive/items/";
string itemByPathUrl = "https://api.onedrive.com/v1.0/drive/root:/";
}
class OneDriveException: Exception
{
@nogc @safe pure nothrow this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null)
{
super(msg, file, line, next);
}
@nogc @safe pure nothrow this(string msg, Throwable next, string file = __FILE__, size_t line = __LINE__)
{
super(msg, file, line, next);
}
}
final class OneDriveApi
{
private string clientId, clientSecret;
private string refreshToken, accessToken;
private HTTP http;
void function(string) onRefreshToken; // called when a new refresh_token is received
this(string clientId, string clientSecret)
{
this.clientId = clientId;
this.clientSecret = clientSecret;
http = HTTP();
//debug http.verbose = true;
// HACK: prevent SIGPIPE
//import etc.c.curl;
//http.handle.set(CurlOption.nosignal, 0);
//signal(/*SIGPIPE*/ 13, /*SIG_IGN*/ cast(void function(int)) 1);
}
~this()
{
http.shutdown();
}
void authorize()
{
import std.stdio, std.regex;
string url = authUrl ~ "?client_id=" ~ clientId ~ "&scope=wl.offline_access onedrive.readwrite&response_type=code&redirect_url=" ~ redirectUrl;
writeln("Authorize this app visiting:");
writeln(url);
while (true) {
char[] response;
write("Enter the response url: ");
readln(response);
auto c = matchFirst(response, r"(?:code=)(([\w\d]+-){4}[\w\d]+)");
if (!c.empty) {
c.popFront(); // skip whole match
redeemToken(c.front);
break;
}
}
}
void setRefreshToken(string refreshToken)
{
this.refreshToken = refreshToken;
newToken();
}
string getItemPath(const(char)[] id)
{
JSONValue response = get(itemByIdUrl ~ id ~ "/?select=name,parentReference");
string path;
try {
path = response["parentReference"].object["path"].str;
} catch (JSONException e) {
// root does not have parentReference
return "";
}
path = decodeComponent(path[path.indexOf(':') + 1 .. $]);
return buildNormalizedPath("." ~ path ~ "/" ~ response["name"].str);
}
string getItemId(const(char)[] path)
{
JSONValue response = get(itemByPathUrl ~ encodeComponent(path) ~ ":/?select=id");
return response["id"].str;
}
// https://dev.onedrive.com/items/view_changes.htm
JSONValue viewChangesById(const(char)[] id, const(char)[] statusToken)
{
char[] url = itemByIdUrl ~ id ~ "/view.changes";
if (statusToken) url ~= "?token=" ~ statusToken;
return get(url);
}
// https://dev.onedrive.com/items/view_changes.htm
JSONValue viewChangesByPath(const(char)[] path, const(char)[] statusToken)
{
char[] url = itemByPathUrl ~ encodeComponent(path).dup ~ ":/view.changes";
url ~= "?select=id,name,eTag,cTag,deleted,file,folder,fileSystemInfo,parentReference";
if (statusToken) url ~= "&token=" ~ statusToken;
return get(url);
}
// https://dev.onedrive.com/items/download.htm
void downloadById(const(char)[] id, string saveToPath)
{
/*string downloadUrl;
// obtain the download url
http.url = itemByIdUrl ~ id ~ "/content";
http.method = HTTP.Method.get;
http.maxRedirects = 0;
http.onReceive = (ubyte[] data) { return data.length; };
http.onReceiveHeader = (in char[] key, in char[] value) {
if (sicmp(key, "location") == 0) {
http.onReceiveHeader = null;
downloadUrl = value.dup;
}
};
writeln("Obtaining the url ...");
http.perform();
check();
http.maxRedirects = 10;
if (downloadUrl) {
// try to download the file
try {
download(downloadUrl, saveToPath);
} catch (CurlException e) {
import std.file;
if (exists(saveToPath)) remove(saveToPath);
throw new OneDriveException("Download error", e);
}
} else {
throw new OneDriveException("Can't obtain the download url");
}*/
char[] url = itemByIdUrl ~ id ~ "/content";
try {
download(url, saveToPath, http);
} catch (CurlException e) {
import std.file;
if (exists(saveToPath)) remove(saveToPath);
throw new OneDriveException("Download error", e);
}
}
// https://dev.onedrive.com/items/upload_put.htm
auto simpleUpload(string localPath, const(char)[] remotePath, const(char)[] eTag = null)
{
char[] url = itemByPathUrl ~ remotePath ~ ":/content";
ubyte[] content;
http.onReceive = (ubyte[] data) {
content ~= data;
return data.length;
};
if (eTag) http.addRequestHeader("If-Match", eTag);
upload(localPath, url, http);
// remove the if-match header
if (eTag) setAccessToken(accessToken);
check();
return parseJSON(content);
}
private void redeemToken(const(char)[] authCode)
{
string postData = "client_id=" ~ clientId ~ "&redirect_url=" ~ redirectUrl ~ "&client_secret=" ~ clientSecret;
postData ~= "&code=" ~ authCode ~ "&grant_type=authorization_code";
acquireToken(postData);
}
private void newToken()
{
string postData = "client_id=" ~ clientId ~ "&redirect_url=" ~ redirectUrl ~ "&client_secret=" ~ clientSecret;
postData ~= "&refresh_token=" ~ refreshToken ~ "&grant_type=refresh_token";
acquireToken(postData);
}
private void acquireToken(const(char)[] postData)
{
JSONValue response = post(tokenUrl, postData);
setAccessToken(response["access_token"].str());
refreshToken = response["refresh_token"].str().dup;
if (onRefreshToken) onRefreshToken(refreshToken);
}
private void setAccessToken(string accessToken)
{
http.clearRequestHeaders();
this.accessToken = accessToken;
http.addRequestHeader("Authorization", "bearer " ~ accessToken);
}
private auto get(const(char)[] url)
{
return parseJSON(.get(url, http));
}
private auto post(T)(const(char)[] url, const(T)[] postData)
{
return parseJSON(.post(url, postData, http));
}
private void check()
{
if (http.statusLine.code / 100 != 2) {
throw new OneDriveException(format("HTTP request returned status code %d (%s)", http.statusLine.code, http.statusLine.reason));
}
}
}

181
src/sqlite.d Normal file
View file

@ -0,0 +1,181 @@
module sqlite;
import etc.c.sqlite3;
import std.string: fromStringz, toStringz;
extern (C) immutable(char)* sqlite3_errstr(int); // missing from the std library
private string ifromStringz(const(char)* cstr)
{
return fromStringz(cstr).dup;
}
class SqliteException: Exception
{
@safe pure nothrow this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null)
{
super(msg, file, line, next);
}
@safe pure nothrow this(string msg, Throwable next, string file = __FILE__, size_t line = __LINE__)
{
super(msg, file, line, next);
}
}
struct Database
{
private sqlite3* pDb;
this(const(char)[] filename)
{
open(filename);
}
~this()
{
close();
}
void open(const(char)[] filename)
{
// https://www.sqlite.org/c3ref/open.html
int rc = sqlite3_open(toStringz(filename), &pDb);
if (rc != SQLITE_OK) {
close();
throw new SqliteException(ifromStringz(sqlite3_errstr(rc)));
}
sqlite3_extended_result_codes(pDb, 1); // always use extended result codes
}
void exec(const(char)[] sql)
{
// https://www.sqlite.org/c3ref/exec.html
int rc = sqlite3_exec(pDb, toStringz(sql), null, null, null);
if (rc != SQLITE_OK) {
throw new SqliteException(ifromStringz(sqlite3_errmsg(pDb)));
}
}
Statement prepare(const(char)[] zSql)
{
Statement s;
// https://www.sqlite.org/c3ref/prepare.html
int rc = sqlite3_prepare_v2(pDb, zSql.ptr, cast(int) zSql.length, &s.pStmt, null);
if (rc != SQLITE_OK) {
throw new SqliteException(ifromStringz(sqlite3_errmsg(pDb)));
}
return s;
}
void close()
{
// https://www.sqlite.org/c3ref/close.html
sqlite3_close_v2(pDb);
pDb = null;
}
}
struct Statement
{
struct Result
{
private sqlite3_stmt* pStmt;
private const(char)[][] row;
private this(sqlite3_stmt* pStmt)
{
this.pStmt = pStmt;
step(); // initialize the range
}
@property bool empty()
{
return row.length == 0;
}
@property auto front()
{
return row;
}
alias step popFront;
void step()
{
// https://www.sqlite.org/c3ref/step.html
int rc = sqlite3_step(pStmt);
if (rc == SQLITE_DONE) {
row.length = 0;
} else if (rc == SQLITE_ROW) {
// https://www.sqlite.org/c3ref/data_count.html
int count = sqlite3_data_count(pStmt);
row = new const(char)[][count];
foreach (int i, ref column; row) {
// https://www.sqlite.org/c3ref/column_blob.html
column = fromStringz(sqlite3_column_text(pStmt, i));
}
} else {
throw new SqliteException(ifromStringz(sqlite3_errmsg(sqlite3_db_handle(pStmt))));
}
}
}
private sqlite3_stmt* pStmt;
~this()
{
// https://www.sqlite.org/c3ref/finalize.html
sqlite3_finalize(pStmt);
}
void bind(int index, const(char)[] value)
{
reset();
// https://www.sqlite.org/c3ref/bind_blob.html
int rc = sqlite3_bind_text(pStmt, index, value.ptr, cast(int) value.length, SQLITE_STATIC);
if (rc != SQLITE_OK) {
throw new SqliteException(ifromStringz(sqlite3_errmsg(sqlite3_db_handle(pStmt))));
}
}
Result exec()
{
reset();
return Result(pStmt);
}
private void reset()
{
// https://www.sqlite.org/c3ref/reset.html
int rc = sqlite3_reset(pStmt);
if (rc != SQLITE_OK) {
throw new SqliteException(ifromStringz(sqlite3_errmsg(sqlite3_db_handle(pStmt))));
}
}
}
unittest
{
auto db = Database(":memory:");
db.exec("CREATE TABLE test(
id TEXT PRIMARY KEY,
value TEXT
)");
auto s = db.prepare("INSERT INTO test VALUES (?, ?)");
s.bind(1, "key1");
s.bind(2, "value1");
s.exec();
s.bind(1, "key2");
s.bind(2, "value2");
s.exec();
s = db.prepare("SELECT * FROM test ORDER BY id ASC");
auto r = s.exec();
assert(r.front[0] == "key1");
r.popFront();
assert(r.front[1] == "value2");
r.popFront();
assert(r.empty);
}

277
src/sync.d Normal file
View file

@ -0,0 +1,277 @@
import std.stdio, std.file, std.json;
import cache, config, onedrive, util;
private string statusTokenFile = "status_token";
private bool isItemFolder(const ref JSONValue item)
{
scope (failure) return false;
JSONValue folder = item["folder"];
return true;
}
private bool isItemFile(const ref JSONValue item)
{
scope (failure) return false;
JSONValue folder = item["file"];
return true;
}
private bool isItemDeleted(const ref JSONValue item)
{
scope (failure) return false;
return !item["deleted"].isNull();
}
class SyncException: Exception
{
@nogc @safe pure nothrow this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null)
{
super(msg, file, line, next);
}
@nogc @safe pure nothrow this(string msg, Throwable next, string file = __FILE__, size_t line = __LINE__)
{
super(msg, file, line, next);
}
}
final class SyncEngine
{
Config cfg;
OneDriveApi onedrive;
ItemCache itemCache;
string[] itemToDelete; // array of items to be deleted
this(Config cfg, OneDriveApi onedrive)
{
assert(onedrive);
this.cfg = cfg;
this.onedrive = onedrive;
itemCache.init();
}
void applyDifferences()
{
string statusToken;
try {
statusToken = readText(statusTokenFile);
} catch (FileException e) {
writeln("Welcome !");
}
writeln("Checking for changes...");
string currDir = getcwd();
string syncDir = cfg.get("sync_dir");
JSONValue changes;
do {
chdir(syncDir);
changes = onedrive.viewChangesByPath("Politecnico", statusToken);
foreach (item; changes["value"].array) {
applyDifference(item);
}
statusToken = changes["@changes.token"].str;
chdir(currDir);
std.file.write(statusTokenFile, statusToken);
} while (changes["@changes.hasMoreChanges"].type == JSON_TYPE.TRUE);
chdir(syncDir);
deleteFiles();
chdir(currDir);
}
private void applyDifference(JSONValue item)
{
string id = item["id"].str;
string name = item["name"].str;
string eTag = item["eTag"].str;
Item cachedItem;
bool cached = itemCache.selectById(id, cachedItem);
// skip items already downloaded
//if (cached && cachedItem.eTag == eTag) return;
writeln("Item ", id, " ", name);
ItemType type;
if (isItemDeleted(item)) {
writeln("The item is marked for deletion");
if (cached) {
applyDelete(cachedItem);
}
return;
} else if (isItemFile(item)) {
type = ItemType.file;
writeln("The item is a file");
} else if (isItemFolder(item)) {
type = ItemType.dir;
writeln("The item is a directory");
} else {
writeln("The item is neither a file nor a directory, skipping");
//skippedFolders ~= id;
return;
}
string cTag = item["cTag"].str;
string mtime = item["fileSystemInfo"].object["lastModifiedDateTime"].str;
string parentId = item["parentReference"].object["id"].str;
string crc32;
if (type == ItemType.file) {
try {
crc32 = item["file"].object["hashes"].object["crc32Hash"].str;
} catch (JSONException e) {
writeln("The hash is not available");
}
}
Item newItem;
itemCache.insert(id, name, type, eTag, cTag, mtime, parentId, crc32);
itemCache.selectById(id, newItem);
writeln("Path: ", newItem.path);
try {
if (!cached) {
applyNewItem(newItem);
} else {
applyChangedItem(cachedItem, newItem);
}
} catch (SyncException e) {
itemCache.deleteById(id);
throw e;
}
}
private void applyDelete(Item item)
{
if (exists(item.path)) {
if (isItemSynced(item, true)) {
addFileToDelete(item.path);
} else {
writeln("The local item is not synced, renaming ...");
safeRename(item.path);
}
} else {
writeln("The local item is already deleted");
}
}
private void applyNewItem(Item item)
{
assert(item.id);
if (exists(item.path)) {
if (isItemSynced(item, true)) {
writeln("The item is already present");
// ensure the modified time is synced
setTimes(item.path, item.mtime, item.mtime);
return;
} else {
writeln("The item is not synced, renaming ...");
safeRename(item.path);
}
}
final switch (item.type) {
case ItemType.file:
writeln("Downloading ...");
try {
onedrive.downloadById(item.id, item.path);
} catch (OneDriveException e) {
throw new SyncException("Sync error", e);
}
break;
case ItemType.dir:
writeln("Creating local directory...");
mkdir(item.path);
break;
}
setTimes(item.path, item.mtime, item.mtime);
}
private void applyChangedItem(Item oldItem, Item newItem)
{
assert(oldItem.id == newItem.id);
if (exists(oldItem.path)) {
if (isItemSynced(oldItem)) {
if (oldItem.eTag != newItem.eTag) {
assert(oldItem.type == newItem.type);
if (oldItem.path != newItem.path) {
writeln("Moved item ", oldItem.path, " to ", newItem.path);
if (exists(newItem.path)) {
writeln("The destination is occupied, renaming ...");
safeRename(newItem.path);
}
rename(oldItem.path, newItem.path);
}
if (oldItem.type == ItemType.file && oldItem.cTag != newItem.cTag) {
writeln("Downloading ...");
onedrive.downloadById(oldItem.id, oldItem.path);
}
setTimes(newItem.path, newItem.mtime, newItem.mtime);
writeln("Updated last modified time");
} else {
writeln("The item is not changed");
}
} else {
writeln("The item is not synced, renaming ...");
safeRename(oldItem.path);
applyNewItem(newItem);
}
} else {
applyNewItem(newItem);
}
}
// returns true if the given item corresponds to the local one
private bool isItemSynced(Item item, bool checkHash = false)
{
final switch (item.type) {
case ItemType.file:
if (isFile(item.path)) {
SysTime localModifiedTime = timeLastModified(item.path);
import core.time: Duration;
item.mtime.fracSecs = Duration.zero; // HACK
if (localModifiedTime == item.mtime) return true;
else {
writeln("The local item has a different modified time ", localModifiedTime, " remote is ", item.mtime);
}
if (checkHash && item.crc32) {
string localCrc32 = computeCrc32(item.path);
if (localCrc32 == item.crc32) return true;
else {
writeln("The local item has a different hash");
}
}
} else {
writeln("The local item is a directory but should be a file");
}
break;
case ItemType.dir:
if (isDir(item.path)) return true;
else {
writeln("The local item is a file but should be a directory");
}
break;
}
return false;
}
private void addFileToDelete(string path)
{
itemToDelete ~= path;
}
private void deleteFiles()
{
foreach_reverse (ref path; itemToDelete) {
if (isFile(path)) {
remove(path);
} else {
// TODO: test not empty folder
rmdir(path);
}
}
assumeSafeAppend(itemToDelete);
itemToDelete.length = 0;
}
}

44
src/util.d Normal file
View file

@ -0,0 +1,44 @@
import std.conv: to;
import std.digest.crc;
import std.digest.digest;
import std.stdio;
import std.string: chomp;
import std.file: exists, rename;
import std.path: extension;
private string deviceName;
static this()
{
import std.socket;
deviceName = Socket.hostName;
}
// give a new name to the specified file or directory
void safeRename(const(char)[] path)
{
auto ext = extension(path);
auto newPath = path.chomp(ext) ~ "-" ~ deviceName;
if (exists(newPath ~ ext)) {
int n = 2;
char[] newPath2;
do {
newPath2 = newPath ~ "-" ~ n.to!string;
n++;
} while (exists(newPath2 ~ ext));
newPath = newPath2;
}
newPath ~= ext;
rename(path, newPath);
}
// return the crc32 hex string of a file
string computeCrc32(string path)
{
CRC32 crc;
auto file = File(path, "rb");
foreach (ubyte[] data; chunks(file, 4096)) {
crc.put(data);
}
return crc.finish().toHexString().dup;
}