mirror of
https://github.com/abraunegg/onedrive
synced 2024-05-08 16:56:39 +02:00
new feature selective sync
removed system wide config file hardcoded sane default config updated README
This commit is contained in:
parent
8b204d18b4
commit
9dd944286f
5
Makefile
5
Makefile
|
@ -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
|
||||||
|
|
34
README.md
34
README.md
|
@ -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
4
config
Normal 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 = ".*|~*"
|
|
@ -1,4 +0,0 @@
|
||||||
client_id = "22c49a0d-d21c-4792-aed1-8f163c982546"
|
|
||||||
sync_dir = "~/OneDrive"
|
|
||||||
skip_file = ".*|~*"
|
|
||||||
skip_dir = ".*"
|
|
25
src/config.d
25
src/config.d
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
95
src/sync.d
95
src/sync.d
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
28
src/util.d
28
src/util.d
|
@ -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"]));
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue