new feature selective sync

removed system wide config file
hardcoded sane default config
updated README
This commit is contained in:
skilion 2017-03-12 16:07:45 +01:00
parent 8b204d18b4
commit 9dd944286f
8 changed files with 133 additions and 68 deletions

View file

@ -1,7 +1,6 @@
DC = dmd DC = dmd
DFLAGS = -ofonedrive -L-lcurl -L-lsqlite3 -L-ldl DFLAGS = -ofonedrive -L-lcurl -L-lsqlite3 -L-ldl
DESTDIR = /usr/local/bin DESTDIR = /usr/local/bin
CONFDIR = /usr/local/etc
SOURCES = \ SOURCES = \
src/config.d \ src/config.d \
@ -27,12 +26,10 @@ unittest: $(SOURCES)
clean: clean:
rm -f onedrive.o onedrive rm -f onedrive.o onedrive
install: onedrive onedrive.conf install: onedrive onedrive.service
install onedrive $(DESTDIR)/onedrive install onedrive $(DESTDIR)/onedrive
install -m 644 onedrive.conf $(CONFDIR)/onedrive.conf
install -m 644 onedrive.service /usr/lib/systemd/user install -m 644 onedrive.service /usr/lib/systemd/user
uninstall: uninstall:
rm -f $(DESTDIR)/onedrive rm -f $(DESTDIR)/onedrive
rm -f $(CONFDIR)/onedrive.conf
rm -f /usr/lib/systemd/user/onedrive.service rm -f /usr/lib/systemd/user/onedrive.service

View file

@ -37,21 +37,30 @@ sudo make install
You should copy the default config file into your home directory before making changes: You should copy the default config file into your home directory before making changes:
``` ```
mkdir -p ~/.config/onedrive mkdir -p ~/.config/onedrive
cp /usr/local/etc/onedrive.conf ~/.config/onedrive/config cp ./config ~/.config/onedrive/config
``` ```
Available options: Available options:
* `client_id`: application identifier necessary for the [authentication][2]
* `sync_dir`: directory where the files will be synced * `sync_dir`: directory where the files will be synced
* `skip_file`: any files that match this pattern will be skipped during sync * `skip_file`: any files or directories that match this pattern will be skipped during sync
* `skip_dir`: any directories that match this pattern will be skipped during sync
Pattern are case insensitive. Pattern are case insensitive.
`*` and `?` [wildcards characters][3] are supported. `*` and `?` [wildcards characters][1] are supported.
Use `|` to separate multiple patterns. Use `|` to separate multiple patterns.
[2]: https://dev.onedrive.com/auth/msa_oauth.htm [1]: https://technet.microsoft.com/en-us/library/bb490639.aspx
[3]: https://technet.microsoft.com/en-us/library/bb490639.aspx
### Selective sync
Selective sync allows you to sync only specific files and directories.
To enable selective sync create a file named `sync_list` in `~/.config/onedrive`.
Each line represents a path to a file or directory relative from your `sync_dir`.
```
$ cat ~/.config/onedrive/sync_list
Backup
Documents/report.odt
Work/ProjectX
notes.txt
```
### First run ### First run
The first time you run the program you will be asked to sign in. The procedure requires a web browser. The first time you run the program you will be asked to sign in. The procedure requires a web browser.
@ -72,16 +81,17 @@ journalctl --user-unit onedrive -f
``` ```
onedrive [OPTION]... onedrive [OPTION]...
no option Sync and exit.
-m --monitor Keep monitoring for local and remote changes. -m --monitor Keep monitoring for local and remote changes.
--resync Forget the last saved state, perform a full sync. --resync Forget the last saved state, perform a full sync.
--logout Logout the current user.
--confdir Set the directory to use to store the configuration files.
-v --verbose Print more details, useful for debugging. -v --verbose Print more details, useful for debugging.
-h --help This help information. -h --help This help information.
``` ```
### Notes: ### Notes:
* After changing the filters (`skip_file` or `skip_dir` in your configs) you must execute `onedrive --resync` * After changing `skip_file` in your configs or the sync list, you must execute `onedrive --resync`
* [Windows naming conventions][4] apply * [Windows naming conventions][2] apply
* Use `make debug` to generate an executable for debugging * Use `make debug` to generate an executable for debugging
[4]: https://msdn.microsoft.com/en-us/library/aa365247 [2]: https://msdn.microsoft.com/en-us/library/aa365247

4
config Normal file
View file

@ -0,0 +1,4 @@
# Directory where the files will be synced
sync_dir = "~/OneDrive"
# Skip files and directories that match this pattern
skip_file = ".*|~*"

View file

@ -1,4 +0,0 @@
client_id = "22c49a0d-d21c-4792-aed1-8f163c982546"
sync_dir = "~/OneDrive"
skip_file = ".*|~*"
skip_dir = ".*"

View file

@ -1,4 +1,4 @@
import std.file, std.regex, std.stdio; import std.file, std.string, std.regex, std.stdio;
static import log; static import log;
final class Config final class Config
@ -7,6 +7,7 @@ final class Config
public string statusTokenFilePath; public string statusTokenFilePath;
public string databaseFilePath; public string databaseFilePath;
public string uploadStateFilePath; public string uploadStateFilePath;
public string syncListFilePath;
private string userConfigFilePath; private string userConfigFilePath;
// hashmap for the values found in the user config file // hashmap for the values found in the user config file
@ -19,15 +20,16 @@ final class Config
databaseFilePath = configDirName ~ "/items.sqlite3"; databaseFilePath = configDirName ~ "/items.sqlite3";
uploadStateFilePath = configDirName ~ "/resume_upload"; uploadStateFilePath = configDirName ~ "/resume_upload";
userConfigFilePath = configDirName ~ "/config"; userConfigFilePath = configDirName ~ "/config";
syncListFilePath = configDirName ~ "/sync_list";
} }
void init() void init()
{ {
bool found = false; setValue("sync_dir", "~/OneDrive");
found |= load("/etc/onedrive.conf"); setValue("skip_file", ".*|~*");
found |= load("/usr/local/etc/onedrive.conf"); if (!load(userConfigFilePath)) {
found |= load(userConfigFilePath); log.vlog("No config file found, using defaults");
if (!found) throw new Exception("No config file found"); }
} }
string getValue(string key) string getValue(string key)
@ -50,12 +52,19 @@ final class Config
} }
} }
void setValue(string key, string value)
{
values[key] = value;
}
private bool load(string filename) private bool load(string filename)
{ {
scope(failure) return false; scope(failure) return false;
auto file = File(filename, "r"); auto file = File(filename, "r");
auto r = regex(`^\s*(\w+)\s*=\s*"(.*)"\s*$`); auto r = regex(`^(\w+)\s*=\s*"(.*)"\s*$`);
foreach (line; file.byLine()) { foreach (line; file.byLine()) {
line = stripLeft(line);
if (line.length == 0 || line[0] == ';' || line[0] == '#') continue;
auto c = line.matchFirst(r); auto c = line.matchFirst(r);
if (!c.empty) { if (!c.empty) {
c.popFront(); // skip the whole match c.popFront(); // skip the whole match
@ -73,7 +82,7 @@ final class Config
unittest unittest
{ {
auto cfg = new Config(""); auto cfg = new Config("");
cfg.load("onedrive.conf"); cfg.load("config");
assert(cfg.getValue("sync_dir") == "~/OneDrive"); assert(cfg.getValue("sync_dir") == "~/OneDrive");
assert(cfg.getValue("empty", "default") == "default"); assert(cfg.getValue("empty", "default") == "default");
} }

View file

@ -114,7 +114,8 @@ final class ItemDatabase
bool selectByPath(const(char)[] path, out Item item) bool selectByPath(const(char)[] path, out Item item)
{ {
path = "root/" ~ path.chompPrefix("."); // HACK // prefix with the root dir
path = "root/" ~ path.chompPrefix(".");
// initialize the search // initialize the search
string[2][] candidates; // [id, parentId] string[2][] candidates; // [id, parentId]
@ -240,8 +241,7 @@ final class ItemDatabase
else path = r.front[0].idup; else path = r.front[0].idup;
} else { } else {
// root // root
if (path) path = "./" ~ path; if (!path) path = ".";
else path = ".";
break; break;
} }
id = r.front[1].dup; id = r.front[1].dup;

View file

@ -51,8 +51,10 @@ final class SyncEngine
private Config cfg; private Config cfg;
private OneDriveApi onedrive; private OneDriveApi onedrive;
private ItemDatabase itemdb; private ItemDatabase itemdb;
private Regex!char skipDir, skipFile;
private UploadSession session; private UploadSession session;
private Regex!char skipFile;
// list of paths to sync
private string[] selectiveSyncPaths;
// token representing the last status correctly synced // token representing the last status correctly synced
private string statusToken; private string statusToken;
// list of items to skip while applying the changes // list of items to skip while applying the changes
@ -66,9 +68,19 @@ final class SyncEngine
this.cfg = cfg; this.cfg = cfg;
this.onedrive = onedrive; this.onedrive = onedrive;
this.itemdb = itemdb; this.itemdb = itemdb;
skipDir = wild2regex(cfg.getValue("skip_dir"));
skipFile = wild2regex(cfg.getValue("skip_file")); skipFile = wild2regex(cfg.getValue("skip_file"));
session = UploadSession(onedrive, cfg.uploadStateFilePath); session = UploadSession(onedrive, cfg.uploadStateFilePath);
// read the selective sync list
if (exists(cfg.syncListFilePath)) {
import std.array;
auto file = File(cfg.syncListFilePath);
selectiveSyncPaths = file
.byLine()
.map!(a => buildNormalizedPath(a))
.filter!(a => a.length > 0)
.array;
}
} }
void init() void init()
@ -146,13 +158,19 @@ final class SyncEngine
parentId = null; parentId = null;
} }
log.vlog(id, " ", name);
// skip unwanted items early // skip unwanted items early
if (skippedItems.find(parentId).length != 0) { if (skippedItems.find(parentId).length != 0) {
log.vlog("Filtered out");
skippedItems ~= id;
return;
}
if (!name.matchFirst(skipFile).empty) {
log.vlog("Filtered out");
skippedItems ~= id; skippedItems ~= id;
return; return;
} }
log.vlog(id, " ", name);
// rename the local item if it is unsynced and there is a new version of it // rename the local item if it is unsynced and there is a new version of it
Item oldItem; Item oldItem;
@ -178,22 +196,19 @@ final class SyncEngine
string path = "."; string path = ".";
if (parentId) { if (parentId) {
path = itemdb.computePath(parentId) ~ "/" ~ name; path = itemdb.computePath(parentId) ~ "/" ~ name;
// selective sync
if (isPathExcluded(path, selectiveSyncPaths)) {
log.vlog("Filtered out: ", path);
skippedItems ~= id;
return;
}
} }
ItemType type; ItemType type;
if (isItemFile(item)) { if (isItemFile(item)) {
type = ItemType.file; type = ItemType.file;
if (!path.matchFirst(skipFile).empty) {
log.vlog("Filtered out");
return;
}
} else if (isItemFolder(item)) { } else if (isItemFolder(item)) {
type = ItemType.dir; type = ItemType.dir;
if (!path.matchFirst(skipDir).empty) {
log.vlog("Filtered out");
skippedItems ~= id;
return;
}
} else { } else {
log.vlog("The item is neither a file nor a directory, skipping"); log.vlog("The item is neither a file nor a directory, skipping");
skippedItems ~= id; skippedItems ~= id;
@ -365,20 +380,23 @@ final class SyncEngine
private void uploadDifferences(Item item) private void uploadDifferences(Item item)
{ {
log.vlog(item.id, " ", item.name); log.vlog(item.id, " ", item.name);
// skip filtered items
if (!item.name.matchFirst(skipFile).empty) {
log.vlog("Filtered out");
return;
}
string path = itemdb.computePath(item.id); string path = itemdb.computePath(item.id);
if (isPathExcluded(path, selectiveSyncPaths)) {
log.vlog("Filtered out: ", path);
return;
}
final switch (item.type) { final switch (item.type) {
case ItemType.dir: case ItemType.dir:
if (!path.matchFirst(skipDir).empty) {
log.vlog("Filtered out");
break;
}
uploadDirDifferences(item, path); uploadDirDifferences(item, path);
break; break;
case ItemType.file: case ItemType.file:
if (!path.matchFirst(skipFile).empty) {
log.vlog("Filtered out");
break;
}
uploadFileDifferences(item, path); uploadFileDifferences(item, path);
break; break;
} }
@ -448,26 +466,33 @@ final class SyncEngine
private void uploadNewItems(string path) private void uploadNewItems(string path)
{ {
// skip unexisting symbolic links
if (isSymlink(path) && !exists(readLink(path))) { if (isSymlink(path) && !exists(readLink(path))) {
return; return;
} }
// skip filtered items
if (!baseName(path).matchFirst(skipFile).empty) {
return;
}
if (isPathExcluded(path, selectiveSyncPaths)) {
return;
}
if (isDir(path)) { if (isDir(path)) {
if (path.matchFirst(skipDir).empty) { Item item;
Item item; if (!itemdb.selectByPath(path, item)) {
if (!itemdb.selectByPath(path, item)) { uploadCreateDir(path);
uploadCreateDir(path); }
} // recursively traverse children
auto entries = dirEntries(path, SpanMode.shallow, false); auto entries = dirEntries(path, SpanMode.shallow, false);
foreach (DirEntry entry; entries) { foreach (DirEntry entry; entries) {
uploadNewItems(entry.name); uploadNewItems(entry.name);
}
} }
} else { } else {
if (path.matchFirst(skipFile).empty) { Item item;
Item item; if (!itemdb.selectByPath(path, item)) {
if (!itemdb.selectByPath(path, item)) { uploadNewFile(path);
uploadNewFile(path);
}
} }
} }
} }

View file

@ -1,3 +1,4 @@
import std.algorithm;
import std.conv; import std.conv;
import std.digest.crc; import std.digest.crc;
import std.file; import std.file;
@ -55,7 +56,7 @@ Regex!char wild2regex(const(char)[] pattern)
{ {
string str; string str;
str.reserve(pattern.length + 2); str.reserve(pattern.length + 2);
str ~= "/"; str ~= "^";
foreach (c; pattern) { foreach (c; pattern) {
switch (c) { switch (c) {
case '*': case '*':
@ -68,7 +69,7 @@ Regex!char wild2regex(const(char)[] pattern)
str ~= "[^/]"; str ~= "[^/]";
break; break;
case '|': case '|':
str ~= "$|/"; str ~= "$|^";
break; break;
default: default:
str ~= c; str ~= c;
@ -98,9 +99,32 @@ bool multiGlobMatch(const(char)[] path, const(char)[] pattern)
return false; return false;
} }
// test if the given path is not included in the allowed paths
// if there are no allowed paths always return false
bool isPathExcluded(string path, string[] allowedPaths)
{
// always allow the root
if (path == ".") return false;
// if there are no allowed paths always return false
if (allowedPaths.empty) return false;
path = buildNormalizedPath(path);
foreach (allowed; allowedPaths) {
auto comm = commonPrefix(path, allowed);
if (comm.length == path.length || comm.length == allowed.length) {
return false;
}
}
return true;
}
unittest unittest
{ {
assert(multiGlobMatch(".hidden", ".*")); assert(multiGlobMatch(".hidden", ".*"));
assert(multiGlobMatch(".hidden", "file|.*")); assert(multiGlobMatch(".hidden", "file|.*"));
assert(!multiGlobMatch("foo.bar", "foo|bar")); assert(!multiGlobMatch("foo.bar", "foo|bar"));
assert(isPathExcluded("Documents2", ["Documents"]));
assert(isPathExcluded("Hello/World", ["Hello/John"]));
assert(!isPathExcluded("Documents", ["Documents"]));
assert(!isPathExcluded("Documents/a.txt", ["Documents"]));
} }