abraunegg-onedrive/src/sync.d

542 lines
14 KiB
D
Raw Normal View History

2015-09-01 21:17:09 +02:00
import core.exception: RangeError;
import std.algorithm, std.datetime, std.file, std.json, std.path, std.regex, std.stdio;
2015-09-14 23:56:14 +02:00
import config, itemdb, onedrive, util;
2015-09-01 20:45:34 +02:00
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();
}
2015-09-08 18:25:41 +02:00
private bool testCrc32(string path, const(char)[] crc32)
{
if (crc32) {
string localCrc32 = computeCrc32(path);
if (crc32 == localCrc32) return true;
}
return false;
}
2015-09-01 20:45:34 +02:00
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
{
2015-09-14 23:56:14 +02:00
private Config cfg;
private OneDriveApi onedrive;
private ItemDatabase itemdb;
private bool verbose;
private Regex!char skipDir, skipFile;
2015-09-18 21:42:27 +02:00
// token representing the last status correctly synced
2015-09-14 23:56:14 +02:00
private string statusToken;
2015-09-18 21:42:27 +02:00
// list of items to skip while applying the changes downloaded
2015-09-16 10:29:20 +02:00
private string[] skippedItems;
2015-09-18 21:42:27 +02:00
// list of items to delete after the changes has been downloaded
2015-09-19 15:38:43 +02:00
private string[] pathsToDelete;
2015-09-14 23:56:14 +02:00
void delegate(string) onStatusToken;
2015-09-01 20:45:34 +02:00
2015-09-14 23:56:14 +02:00
this(Config cfg, OneDriveApi onedrive, ItemDatabase itemdb, bool verbose)
2015-09-01 20:45:34 +02:00
{
2015-09-14 23:56:14 +02:00
assert(onedrive && itemdb);
2015-09-01 20:45:34 +02:00
this.cfg = cfg;
this.onedrive = onedrive;
2015-09-14 23:56:14 +02:00
this.itemdb = itemdb;
this.verbose = verbose;
2015-09-19 09:45:45 +02:00
skipDir = regex(wild2regex(cfg.get("skip_dir", "")));
skipFile = regex(wild2regex(cfg.get("skip_file", "")));
2015-09-01 20:45:34 +02:00
}
2015-09-14 23:56:14 +02:00
void setStatusToken(string statusToken)
2015-09-01 20:45:34 +02:00
{
2015-09-14 23:56:14 +02:00
this.statusToken = statusToken;
}
2015-09-01 20:45:34 +02:00
2015-09-14 23:56:14 +02:00
void applyDifferences()
{
if (verbose) writeln("Applying differences ...");
2015-09-01 20:45:34 +02:00
JSONValue changes;
do {
2015-09-14 23:56:14 +02:00
changes = onedrive.viewChangesByPath("/", statusToken);
2015-09-01 20:45:34 +02:00
foreach (item; changes["value"].array) {
applyDifference(item);
}
statusToken = changes["@changes.token"].str;
2015-09-14 23:56:14 +02:00
onStatusToken(statusToken);
2015-09-01 20:45:34 +02:00
} while (changes["@changes.hasMoreChanges"].type == JSON_TYPE.TRUE);
2015-09-19 15:38:43 +02:00
// delete items in pathsToDelete
if (pathsToDelete.length > 0) deleteItems();
2015-09-16 10:29:20 +02:00
// empty the skipped items
skippedItems.length = 0;
assumeSafeAppend(skippedItems);
2015-09-01 20:45:34 +02:00
}
private void applyDifference(JSONValue item)
{
string id = item["id"].str;
string name = item["name"].str;
2015-09-19 15:38:43 +02:00
string parentId = item["parentReference"].object["id"].str;
// HACK: recognize the root directory
if (name == "root" && parentId[$ - 1] == '0' && parentId[$ - 2] == '!') {
parentId = null;
}
// skip unwanted items early
if (skippedItems.find(parentId).length != 0) {
skippedItems ~= id;
return;
}
2015-09-01 20:45:34 +02:00
2015-09-14 23:56:14 +02:00
if (verbose) writeln(id, " ", name);
2015-09-19 15:38:43 +02:00
// check if the cached item is still synced
Item oldItem;
string oldPath;
bool cached = itemdb.selectById(id, oldItem);
if (cached) {
oldPath = itemdb.computePath(id);
if (!isItemSynced(oldItem, oldPath)) {
if (verbose) writeln("The local item is out of sync, renaming");
if (exists(oldPath)) safeRename(oldPath);
cached = false;
}
}
2015-09-14 23:56:14 +02:00
2015-09-19 15:38:43 +02:00
// compute the path of the item
string path;
if (parentId) {
path = itemdb.computePath(parentId) ~ "/" ~ name;
} else {
path = name;
2015-09-14 23:56:14 +02:00
}
2015-09-01 20:45:34 +02:00
ItemType type;
if (isItemDeleted(item)) {
2015-09-14 23:56:14 +02:00
if (verbose) writeln("The item is marked for deletion");
2015-09-19 15:38:43 +02:00
if (cached) {
itemdb.deleteById(id);
pathsToDelete ~= oldPath;
}
2015-09-01 20:45:34 +02:00
return;
} else if (isItemFile(item)) {
type = ItemType.file;
2015-09-19 15:38:43 +02:00
if (!path.matchFirst(skipFile).empty) {
if (verbose) writeln("Filtered out");
return;
}
2015-09-01 20:45:34 +02:00
} else if (isItemFolder(item)) {
type = ItemType.dir;
2015-09-19 15:38:43 +02:00
if (!path.matchFirst(skipDir).empty) {
if (verbose) writeln("Filtered out");
skippedItems ~= id;
return;
}
2015-09-01 20:45:34 +02:00
} else {
2015-09-17 00:16:23 +02:00
if (verbose) writeln("The item is neither a file nor a directory, skipping");
2015-09-16 10:29:20 +02:00
skippedItems ~= id;
return;
}
2015-09-19 15:38:43 +02:00
string eTag = item["eTag"].str;
2015-09-01 20:45:34 +02:00
string cTag = item["cTag"].str;
string mtime = item["fileSystemInfo"].object["lastModifiedDateTime"].str;
string crc32;
if (type == ItemType.file) {
try {
crc32 = item["file"].object["hashes"].object["crc32Hash"].str;
} catch (JSONException e) {
2015-09-14 23:56:14 +02:00
if (verbose) writeln("The hash is not available");
2015-09-01 21:17:09 +02:00
} catch (RangeError e) {
2015-09-14 23:56:14 +02:00
if (verbose) writeln("The crc32 hash is not available");
2015-09-01 20:45:34 +02:00
}
}
2015-09-19 15:38:43 +02:00
Item newItem = {
id: id,
name: name,
type: type,
eTag: eTag,
cTag: cTag,
mtime: SysTime.fromISOExtString(mtime),
parentId: parentId,
crc32: crc32
};
if (!cached) {
applyNewItem(newItem, path);
2015-09-17 16:28:24 +02:00
} else {
2015-09-19 15:38:43 +02:00
applyChangedItem(oldItem, newItem, path);
2015-09-17 16:28:24 +02:00
}
2015-09-01 20:45:34 +02:00
2015-09-19 15:38:43 +02:00
// save the item in the db
if (oldItem.id) {
itemdb.update(id, name, type, eTag, cTag, mtime, parentId, crc32);
} else {
itemdb.insert(id, name, type, eTag, cTag, mtime, parentId, crc32);
2015-09-01 20:45:34 +02:00
}
}
2015-09-19 15:38:43 +02:00
private void applyNewItem(Item item, string path)
2015-09-01 20:45:34 +02:00
{
2015-09-19 15:38:43 +02:00
if (exists(path)) {
if (isItemSynced(item, path)) {
2015-09-14 23:56:14 +02:00
if (verbose) writeln("The item is already present");
// ensure the modified time is correct
2015-09-19 15:38:43 +02:00
setTimes(path, item.mtime, item.mtime);
2015-09-01 20:45:34 +02:00
return;
} else {
2015-09-14 23:56:14 +02:00
if (verbose) writeln("The local item is out of sync, renaming ...");
2015-09-19 15:38:43 +02:00
safeRename(path);
2015-09-01 20:45:34 +02:00
}
}
final switch (item.type) {
case ItemType.file:
2015-09-19 15:38:43 +02:00
writeln("Downloading: ", path);
2015-09-01 20:45:34 +02:00
try {
2015-09-19 15:38:43 +02:00
onedrive.downloadById(item.id, path);
2015-09-01 20:45:34 +02:00
} catch (OneDriveException e) {
throw new SyncException("Sync error", e);
}
break;
case ItemType.dir:
2015-09-19 15:38:43 +02:00
writeln("Creating directory: ", path);
mkdir(path);
2015-09-01 20:45:34 +02:00
break;
}
2015-09-19 15:38:43 +02:00
setTimes(path, item.mtime, item.mtime);
2015-09-01 20:45:34 +02:00
}
2015-09-19 15:38:43 +02:00
private void applyChangedItem(Item oldItem, Item newItem, string newPath)
2015-09-01 20:45:34 +02:00
{
assert(oldItem.id == newItem.id);
2015-09-14 23:56:14 +02:00
assert(oldItem.type == newItem.type);
if (oldItem.eTag != newItem.eTag) {
2015-09-19 15:38:43 +02:00
string oldPath = itemdb.computePath(oldItem.id);
if (oldPath != newPath) {
writeln("Moving: ", oldPath, " -> ", newPath);
if (exists(newPath)) {
2015-09-14 23:56:14 +02:00
if (verbose) writeln("The destination is occupied, renaming ...");
2015-09-19 15:38:43 +02:00
safeRename(newPath);
2015-09-01 20:45:34 +02:00
}
2015-09-19 15:38:43 +02:00
rename(oldPath, newPath);
2015-09-01 20:45:34 +02:00
}
2015-09-14 23:56:14 +02:00
if (newItem.type == ItemType.file && oldItem.cTag != newItem.cTag) {
2015-09-19 15:38:43 +02:00
writeln("Downloading: ", newPath);
onedrive.downloadById(newItem.id, newPath);
2015-09-14 23:56:14 +02:00
}
2015-09-19 15:38:43 +02:00
setTimes(newPath, newItem.mtime, newItem.mtime);
2015-09-01 20:45:34 +02:00
} else {
2015-09-18 21:42:27 +02:00
if (verbose) writeln("The item has not changed");
2015-09-01 20:45:34 +02:00
}
}
// returns true if the given item corresponds to the local one
2015-09-19 15:38:43 +02:00
private bool isItemSynced(Item item, string path)
2015-09-01 20:45:34 +02:00
{
2015-09-19 15:38:43 +02:00
if (!exists(path)) return false;
2015-09-01 20:45:34 +02:00
final switch (item.type) {
case ItemType.file:
2015-09-19 15:38:43 +02:00
if (isFile(path)) {
SysTime localModifiedTime = timeLastModified(path);
2015-09-01 20:45:34 +02:00
import core.time: Duration;
item.mtime.fracSecs = Duration.zero; // HACK
2015-09-14 23:56:14 +02:00
if (localModifiedTime == item.mtime) {
return true;
} else {
if (verbose) writeln("The local item has a different modified time ", localModifiedTime, " remote is ", item.mtime);
2015-09-01 20:45:34 +02:00
}
2015-09-19 15:38:43 +02:00
if (testCrc32(path, item.crc32)) {
return true;
} else {
if (verbose) writeln("The local item has a different hash");
2015-09-01 20:45:34 +02:00
}
} else {
2015-09-14 23:56:14 +02:00
if (verbose) writeln("The local item is a directory but should be a file");
2015-09-01 20:45:34 +02:00
}
break;
case ItemType.dir:
2015-09-19 15:38:43 +02:00
if (isDir(path)) {
2015-09-14 23:56:14 +02:00
return true;
} else {
if (verbose) writeln("The local item is a file but should be a directory");
2015-09-01 20:45:34 +02:00
}
break;
}
return false;
}
2015-09-14 23:56:14 +02:00
private void deleteItems()
2015-09-01 20:45:34 +02:00
{
2015-09-14 23:56:14 +02:00
if (verbose) writeln("Deleting files ...");
2015-09-19 15:38:43 +02:00
foreach_reverse (path; pathsToDelete) {
2015-09-14 23:56:14 +02:00
if (exists(path)) {
if (isFile(path)) {
remove(path);
writeln("Deleted file: ", path);
}
2015-09-01 20:45:34 +02:00
} else {
2015-09-06 22:42:44 +02:00
try {
rmdir(path);
2015-09-14 23:56:14 +02:00
writeln("Deleted dir: ", path);
2015-09-06 22:42:44 +02:00
} catch (FileException e) {
2015-09-14 23:56:14 +02:00
writeln("Keeping dir: ", path);
2015-09-06 22:42:44 +02:00
}
2015-09-01 20:45:34 +02:00
}
}
2015-09-19 15:38:43 +02:00
pathsToDelete.length = 0;
assumeSafeAppend(pathsToDelete);
2015-09-01 20:45:34 +02:00
}
2015-09-04 21:00:22 +02:00
2015-09-18 21:42:27 +02:00
// scan the given directory for differences
public void scanForDifferences(string path)
2015-09-04 21:00:22 +02:00
{
2015-09-16 10:29:20 +02:00
if (verbose) writeln("Uploading differences ...");
2015-09-18 21:42:27 +02:00
Item item;
if (itemdb.selectByPath(path, item)) {
uploadDifferences(item);
2015-09-08 18:25:41 +02:00
}
2015-09-17 00:16:23 +02:00
if (verbose) writeln("Uploading new items ...");
2015-09-18 21:42:27 +02:00
uploadNewItems(path);
2015-09-04 21:00:22 +02:00
}
2015-09-18 21:42:27 +02:00
public void uploadDifferences(Item item)
2015-09-11 18:33:22 +02:00
{
2015-09-18 21:42:27 +02:00
if (verbose) writeln(item.id, " ", item.name);
2015-09-19 15:38:43 +02:00
string path = itemdb.computePath(item.id);
2015-09-18 21:42:27 +02:00
final switch (item.type) {
case ItemType.dir:
2015-09-19 15:38:43 +02:00
if (!path.matchFirst(skipDir).empty) {
2015-09-19 09:45:45 +02:00
if (verbose) writeln("Filtered out");
break;
}
2015-09-19 15:38:43 +02:00
uploadDirDifferences(item, path);
2015-09-18 21:42:27 +02:00
break;
case ItemType.file:
2015-09-19 15:38:43 +02:00
if (!path.matchFirst(skipFile).empty) {
2015-09-19 09:45:45 +02:00
if (verbose) writeln("Filtered out");
break;
}
2015-09-19 15:38:43 +02:00
uploadFileDifferences(item, path);
2015-09-18 21:42:27 +02:00
break;
2015-09-11 18:33:22 +02:00
}
}
2015-09-19 15:38:43 +02:00
private void uploadDirDifferences(Item item, string path)
2015-09-04 21:00:22 +02:00
{
2015-09-18 21:42:27 +02:00
assert(item.type == ItemType.dir);
2015-09-19 15:38:43 +02:00
if (exists(path)) {
if (!isDir(path)) {
2015-09-18 21:42:27 +02:00
if (verbose) writeln("The item was a directory but now is a file");
2015-09-19 15:38:43 +02:00
uploadDeleteItem(item, path);
uploadNewFile(path);
2015-09-18 21:42:27 +02:00
} else {
if (verbose) writeln("The directory has not changed");
// loop trough the children
foreach (Item child; itemdb.selectChildren(item.id)) {
uploadDifferences(child);
}
}
2015-09-18 21:42:27 +02:00
} else {
if (verbose) writeln("The directory has been deleted");
2015-09-19 15:38:43 +02:00
uploadDeleteItem(item, path);
2015-09-18 21:42:27 +02:00
}
}
2015-09-19 15:38:43 +02:00
private void uploadFileDifferences(Item item, string path)
2015-09-18 21:42:27 +02:00
{
assert(item.type == ItemType.file);
2015-09-19 15:38:43 +02:00
if (exists(path)) {
if (isFile(path)) {
SysTime localModifiedTime = timeLastModified(path);
2015-09-18 21:42:27 +02:00
import core.time: Duration;
item.mtime.fracSecs = Duration.zero; // HACK
if (localModifiedTime != item.mtime) {
if (verbose) writeln("The file last modified time has changed");
string id = item.id;
string eTag = item.eTag;
2015-09-19 15:38:43 +02:00
if (!testCrc32(path, item.crc32)) {
2015-09-18 21:42:27 +02:00
if (verbose) writeln("The file content has changed");
2015-09-19 15:38:43 +02:00
writeln("Uploading: ", path);
auto res = onedrive.simpleUpload(path, path, item.eTag);
2015-09-18 21:42:27 +02:00
saveItem(res);
id = res["id"].str;
eTag = res["eTag"].str;
2015-09-17 16:28:24 +02:00
}
2015-09-18 21:42:27 +02:00
uploadLastModifiedTime(id, eTag, localModifiedTime.toUTC());
2015-09-06 22:42:44 +02:00
} else {
2015-09-18 21:42:27 +02:00
if (verbose) writeln("The file has not changed");
2015-09-06 22:42:44 +02:00
}
2015-09-18 21:42:27 +02:00
} else {
if (verbose) writeln("The item was a file but now is a directory");
2015-09-19 15:38:43 +02:00
uploadDeleteItem(item, path);
uploadCreateDir(path);
2015-09-04 21:00:22 +02:00
}
} else {
2015-09-18 21:42:27 +02:00
if (verbose) writeln("The file has been deleted");
2015-09-19 15:38:43 +02:00
uploadDeleteItem(item, path);
2015-09-11 18:33:22 +02:00
}
}
2015-09-18 21:42:27 +02:00
private void uploadNewItems(string path)
2015-09-08 18:25:41 +02:00
{
2015-09-18 21:42:27 +02:00
if (isDir(path)) {
2015-09-19 15:38:43 +02:00
if (path.matchFirst(skipDir).empty) {
2015-09-18 21:42:27 +02:00
import std.string: chompPrefix;
path = chompPrefix(path, "./");
Item item;
if (!itemdb.selectByPath(path, item)) {
2015-09-17 16:28:24 +02:00
uploadCreateDir(path);
2015-09-18 21:42:27 +02:00
}
auto entries = dirEntries(path, SpanMode.shallow, false);
foreach (DirEntry entry; entries) {
uploadNewItems(entry.name);
}
}
} else {
2015-09-19 15:38:43 +02:00
if (path.matchFirst(skipFile).empty) {
2015-09-18 21:42:27 +02:00
Item item;
if (!itemdb.selectByPath(path, item)) {
2015-09-17 16:28:24 +02:00
uploadNewFile(path);
2015-09-18 21:42:27 +02:00
}
2015-09-08 18:25:41 +02:00
}
2015-09-06 22:42:44 +02:00
}
2015-09-08 18:25:41 +02:00
}
2015-09-18 21:42:27 +02:00
private void uploadCreateDir(const(char)[] path)
2015-09-08 18:25:41 +02:00
{
2015-09-16 10:29:20 +02:00
writeln("Creating remote directory: ", path);
JSONValue item = ["name": baseName(path).idup];
2015-09-14 12:57:47 +02:00
item["folder"] = parseJSON("{}");
auto res = onedrive.createByPath(dirName(path), item);
2015-09-16 10:29:20 +02:00
saveItem(res);
}
private void uploadNewFile(string path)
{
writeln("Uploading: ", path);
2015-09-17 00:16:23 +02:00
JSONValue res;
try {
res = onedrive.simpleUpload(path, path);
} catch (OneDriveException e) {
writeln(e.msg);
return;
}
2015-09-16 10:29:20 +02:00
saveItem(res);
string id = res["id"].str;
string eTag = res["eTag"].str;
2015-09-17 16:28:24 +02:00
SysTime mtime;
try {
mtime = timeLastModified(path).toUTC();
} catch (FileException e) {
writeln(e.msg);
return;
}
uploadLastModifiedTime(id, eTag, mtime);
2015-09-16 10:29:20 +02:00
}
2015-09-19 15:38:43 +02:00
private void uploadDeleteItem(Item item, const(char)[] path)
2015-09-16 10:29:20 +02:00
{
2015-09-19 15:38:43 +02:00
writeln("Deleting remote item: ", path);
2015-09-16 10:29:20 +02:00
onedrive.deleteById(item.id, item.eTag);
itemdb.deleteById(item.id);
2015-09-08 18:25:41 +02:00
}
2015-09-16 10:29:20 +02:00
private void uploadLastModifiedTime(const(char)[] id, const(char)[] eTag, SysTime mtime)
2015-09-08 18:25:41 +02:00
{
JSONValue mtimeJson = [
"fileSystemInfo": JSONValue([
"lastModifiedDateTime": mtime.toISOExtString()
])
];
2015-09-11 18:33:22 +02:00
auto res = onedrive.updateById(id, mtimeJson, eTag);
2015-09-16 10:29:20 +02:00
saveItem(res);
2015-09-08 18:25:41 +02:00
}
2015-09-16 10:29:20 +02:00
private void saveItem(JSONValue item)
2015-09-08 18:25:41 +02:00
{
2015-09-16 10:29:20 +02:00
string id = item["id"].str;
ItemType type;
if (isItemFile(item)) {
type = ItemType.file;
} else if (isItemFolder(item)) {
type = ItemType.dir;
} else {
assert(0);
}
string name = item["name"].str;
string eTag = item["eTag"].str;
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) {
// swallow exception
} catch (RangeError e) {
// swallow exception
}
}
2015-09-17 16:28:24 +02:00
itemdb.upsert(id, name, type, eTag, cTag, mtime, parentId, crc32);
2015-09-16 10:29:20 +02:00
}
2015-09-19 15:38:43 +02:00
void uploadMoveItem(string from, string to)
2015-09-16 10:29:20 +02:00
{
2015-09-17 00:16:23 +02:00
writeln("Moving remote item: ", from, " -> ", to);
2015-09-11 18:33:22 +02:00
Item item;
2015-09-19 15:38:43 +02:00
if (!itemdb.selectByPath(from, item) || !isItemSynced(item, from)) {
2015-09-17 00:16:23 +02:00
writeln("Can't move an unsynced item");
return;
2015-09-11 18:33:22 +02:00
}
2015-09-17 16:28:24 +02:00
if (itemdb.selectByPath(to, item)) {
2015-09-19 15:38:43 +02:00
uploadDeleteItem(item, to);
2015-09-17 16:28:24 +02:00
}
2015-09-11 18:33:22 +02:00
JSONValue diff = ["name": baseName(to)];
diff["parentReference"] = JSONValue([
"path": "/drive/root:/" ~ dirName(to)
]);
auto res = onedrive.updateById(item.id, diff, item.eTag);
2015-09-16 10:29:20 +02:00
saveItem(res);
2015-09-17 16:28:24 +02:00
string id = res["id"].str;
string eTag = res["eTag"].str;
uploadLastModifiedTime(id, eTag, timeLastModified(to).toUTC());
2015-09-11 18:33:22 +02:00
}
void deleteByPath(const(char)[] path)
{
Item item;
2015-09-14 23:56:14 +02:00
if (!itemdb.selectByPath(path, item)) {
2015-09-17 00:16:23 +02:00
throw new SyncException("Can't delete an unsynced item");
2015-09-11 18:33:22 +02:00
}
2015-09-19 15:38:43 +02:00
uploadDeleteItem(item, path);
2015-09-11 18:33:22 +02:00
}
2015-09-01 20:45:34 +02:00
}