From b75f57a6176b74ae1de2f06cfc74ce400c7b312d Mon Sep 17 00:00:00 2001
From: Victor Andreasson <victor.andreasson@fripost.org>
Date: Sat, 24 Aug 2024 11:47:49 +0200
Subject: [PATCH] Refactor: Clarify some variable names

Now that we support repo subdirectories, what previously could be
referred to as a "file name" should now (in most places) be referred to
as a "repo-relative path", i.e. a path relative to the repository root.
---
 .../android/repos/DirectoryRepoTest.java      |  4 +-
 .../orgzly/android/repos/DropboxRepoTest.java |  3 +-
 .../orgzly/android/repos/LocalDbRepoTest.java |  7 +-
 .../com/orgzly/android/repos/SyncTest.java    |  2 +-
 .../java/com/orgzly/android/BookName.java     | 55 +++++++++------
 .../java/com/orgzly/android/LocalStorage.java |  2 +-
 .../com/orgzly/android/data/DataRepository.kt | 22 +++---
 .../android/git/GitFileSynchronizer.java      | 70 +++++++++----------
 .../orgzly/android/repos/DatabaseRepo.java    | 10 +--
 .../orgzly/android/repos/DirectoryRepo.java   | 14 ++--
 .../orgzly/android/repos/DocumentRepo.java    | 38 +++++-----
 .../orgzly/android/repos/DropboxClient.java   | 15 ++--
 .../com/orgzly/android/repos/DropboxRepo.java | 18 ++---
 .../com/orgzly/android/repos/GitRepo.java     | 44 ++++++------
 .../com/orgzly/android/repos/MockRepo.java    | 27 +++++--
 .../orgzly/android/repos/RepoIgnoreNode.kt    |  2 +-
 .../com/orgzly/android/repos/RepoUtils.java   |  4 +-
 .../com/orgzly/android/repos/SyncRepo.java    | 18 +++--
 .../com/orgzly/android/repos/WebdavRepo.kt    | 24 +++----
 .../com/orgzly/android/sync/BookNamesake.java |  4 +-
 .../java/com/orgzly/android/sync/SyncUtils.kt | 14 ++--
 .../orgzly/android/ui/books/BooksFragment.kt  |  4 +-
 .../orgzly/android/usecase/LinkFindTarget.kt  |  2 +-
 .../com/orgzly/android/util/MiscUtils.java    | 16 +++++
 .../com/orgzly/android/util/UriUtils.java     |  4 +-
 25 files changed, 238 insertions(+), 185 deletions(-)

diff --git a/app/src/androidTest/java/com/orgzly/android/repos/DirectoryRepoTest.java b/app/src/androidTest/java/com/orgzly/android/repos/DirectoryRepoTest.java
index f8d59232c..a26ae2a5c 100644
--- a/app/src/androidTest/java/com/orgzly/android/repos/DirectoryRepoTest.java
+++ b/app/src/androidTest/java/com/orgzly/android/repos/DirectoryRepoTest.java
@@ -63,7 +63,7 @@ public void testStoringFile() throws IOException {
 
         assertEquals(1, books.size());
         assertEquals("booky", BookName.fromRook(books.get(0)).getName());
-        assertEquals("booky.org", BookName.fromRook(books.get(0)).getFileName());
+        assertEquals("booky.org", BookName.fromRook(books.get(0)).getRepoRelativePath());
         assertEquals(repoUriString, books.get(0).getRepoUri().toString());
         assertEquals(repoUriString + "/booky.org", books.get(0).getUri().toString());
     }
@@ -80,7 +80,7 @@ public void testExtension() throws IOException {
 
         assertEquals(1, books.size());
         assertEquals("03", BookName.fromRook(books.get(0)).getName());
-        assertEquals("03.org", BookName.fromRook(books.get(0)).getFileName());
+        assertEquals("03.org", BookName.fromRook(books.get(0)).getRepoRelativePath());
         assertEquals(13, books.get(0).getRepoId());
         assertEquals(repoUriString, books.get(0).getRepoUri().toString());
         assertEquals(repoUriString + "/03.org", books.get(0).getUri().toString());
diff --git a/app/src/androidTest/java/com/orgzly/android/repos/DropboxRepoTest.java b/app/src/androidTest/java/com/orgzly/android/repos/DropboxRepoTest.java
index 070e75c9a..6a85e56a5 100644
--- a/app/src/androidTest/java/com/orgzly/android/repos/DropboxRepoTest.java
+++ b/app/src/androidTest/java/com/orgzly/android/repos/DropboxRepoTest.java
@@ -137,8 +137,7 @@ public void testDropboxFileRename() throws IOException {
 
         assertEquals(1, repo.getBooks().size());
         assertEquals(repo.getUri() + "/notebook-renamed.org", repo.getBooks().get(0).getUri().toString());
-        assertEquals("notebook-renamed.org",
-                BookName.fromRook(repo.getBooks().get(0)).getFileName());
+        assertEquals("notebook-renamed.org", BookName.fromRook(repo.getBooks().get(0)).getRepoRelativePath());
     }
 
     private void uploadFileToRepo(Uri repoUri, String fileName, String fileContents) throws IOException {
diff --git a/app/src/androidTest/java/com/orgzly/android/repos/LocalDbRepoTest.java b/app/src/androidTest/java/com/orgzly/android/repos/LocalDbRepoTest.java
index dd54e3c02..9fbcd9978 100644
--- a/app/src/androidTest/java/com/orgzly/android/repos/LocalDbRepoTest.java
+++ b/app/src/androidTest/java/com/orgzly/android/repos/LocalDbRepoTest.java
@@ -8,6 +8,7 @@
 import com.orgzly.android.NotesOrgExporter;
 import com.orgzly.android.OrgzlyTest;
 import com.orgzly.android.db.entity.Book;
+import com.orgzly.android.db.entity.BookView;
 import com.orgzly.android.db.entity.Repo;
 import com.orgzly.android.sync.SyncUtils;
 import com.orgzly.android.util.MiscUtils;
@@ -52,13 +53,13 @@ public void testStoringBook() throws IOException {
         long now = System.currentTimeMillis();
 
         /* Write local book's content to a temporary file. */
-        Book book = dataRepository.getBook("local-book-1");
+        BookView bookView = dataRepository.getBookView("local-book-1");
         File tmpFile = dataRepository.getTempBookFile();
 
         try {
-            new NotesOrgExporter(dataRepository).exportBook(book, tmpFile);
+            new NotesOrgExporter(dataRepository).exportBook(bookView.getBook(), tmpFile);
             repo = testUtils.repoInstance(RepoType.MOCK, "mock://repo-a");
-            repo.storeBook(tmpFile, BookName.fileName(book.getName(), BookFormat.ORG));
+            repo.storeBook(tmpFile, BookName.getRepoRelativePath(bookView));
         } finally {
             tmpFile.delete();
         }
diff --git a/app/src/androidTest/java/com/orgzly/android/repos/SyncTest.java b/app/src/androidTest/java/com/orgzly/android/repos/SyncTest.java
index ff0be9b26..31fa1aaca 100644
--- a/app/src/androidTest/java/com/orgzly/android/repos/SyncTest.java
+++ b/app/src/androidTest/java/com/orgzly/android/repos/SyncTest.java
@@ -431,7 +431,7 @@ public void testDirectoryFileRename() throws IOException {
 
         assertEquals(1, repo.getBooks().size());
         assertEquals(repo.getUri() + "/notebook-renamed.org", repo.getBooks().get(0).getUri().toString());
-        assertEquals("notebook-renamed.org", BookName.fromRook(repo.getBooks().get(0)).getFileName());
+        assertEquals("notebook-renamed.org", BookName.fromRook(repo.getBooks().get(0)).getRepoRelativePath());
 
         LocalStorage.deleteRecursive(new File(repoDir));
     }
diff --git a/app/src/main/java/com/orgzly/android/BookName.java b/app/src/main/java/com/orgzly/android/BookName.java
index e2cf421af..be99f2ed1 100644
--- a/app/src/main/java/com/orgzly/android/BookName.java
+++ b/app/src/main/java/com/orgzly/android/BookName.java
@@ -7,6 +7,7 @@
 import com.orgzly.BuildConfig;
 import com.orgzly.android.db.entity.BookView;
 import com.orgzly.android.repos.Rook;
+import com.orgzly.android.repos.VersionedRook;
 import com.orgzly.android.util.LogUtils;
 
 import java.util.regex.Matcher;
@@ -22,34 +23,39 @@ public class BookName {
     private static final Pattern PATTERN = Pattern.compile("(.*)\\.(org)(\\.txt)?$");
     private static final Pattern SKIP_PATTERN = Pattern.compile("^\\.#.*");
 
-    private final String mFileName;
+    private final String mRepoRelativePath;
     private final String mName;
     private final BookFormat mFormat;
 
-    private BookName(String fileName, String name, BookFormat format) {
-        mFileName = fileName;
+    private BookName(String repoRelativePath, String name, BookFormat format) {
+        mRepoRelativePath = repoRelativePath;
         mName = name;
         mFormat = format;
     }
 
-    public static String getFileName(BookView bookView) {
+    public static String getRepoRelativePath(BookView bookView) {
         if (bookView.getSyncedTo() != null) {
-            return getFileName(bookView.getSyncedTo().getRepoUri(), bookView.getSyncedTo().getUri());
-
+            VersionedRook vrook = bookView.getSyncedTo();
+            return getRepoRelativePath(vrook.getRepoUri(), vrook.getUri());
         } else {
-            return fileName(bookView.getBook().getName(), BookFormat.ORG);
+            // There is no remote book; we can only guess the repo path from the book's name.
+            return repoRelativePath(bookView.getBook().getName(), BookFormat.ORG);
         }
     }
 
+    /**
+     * Used when creating a Book from an imported file.
+     * @param context Used for getting a DocumentFile, if possible
+     * @param uri URI provided by the file picker
+     * @return The book's file name
+     */
     public static String getFileName(Context context, Uri uri) {
         String fileName;
-
         DocumentFile documentFile = DocumentFile.fromSingleUri(context, uri);
 
         if ("content".equals(uri.getScheme()) && documentFile != null) {
             // Try using DocumentFile first (KitKat and above)
             fileName = documentFile.getName();
-
         } else { // Just get the last path segment
             fileName = uri.getLastPathSegment();
         }
@@ -63,7 +69,7 @@ public static String getFileName(Context context, Uri uri) {
         return fileName;
     }
 
-    public static String getFileName(Uri repoUri, Uri fileUri) {
+    public static String getRepoRelativePath(Uri repoUri, Uri fileUri) {
         /* The content:// repository type requires special handling */
         if ("content".equals(repoUri.getScheme())) {
             String repoUriLastSegment = repoUri.toString().replaceAll("^.*/", "");
@@ -79,37 +85,44 @@ public static String getFileName(Uri repoUri, Uri fileUri) {
     }
 
     public static BookName fromRook(Rook rook) {
-        return fromFileName(getFileName(rook.getRepoUri(), rook.getUri()));
+        return fromRepoRelativePath(getRepoRelativePath(rook.getRepoUri(), rook.getUri()));
     }
 
-    public static boolean isSupportedFormatFileName(String fileName) {
-        return PATTERN.matcher(fileName).matches() && !SKIP_PATTERN.matcher(fileName).matches();
+    public static boolean isSupportedFormatFileName(String path) {
+        return PATTERN.matcher(path).matches() && !SKIP_PATTERN.matcher(path).matches();
     }
 
-    public static String fileName(String name, BookFormat format) {
+    public static String repoRelativePath(String name, BookFormat format) {
         if (format == BookFormat.ORG) {
             return name + ".org";
+        } else {
+            throw new IllegalArgumentException("Unsupported format " + format);
+        }
+    }
 
+    public static String lastPathSegment(String name, BookFormat format) {
+        if (format == BookFormat.ORG) {
+            return Uri.parse(name).getLastPathSegment() + ".org";
         } else {
             throw new IllegalArgumentException("Unsupported format " + format);
         }
     }
 
-    public static BookName fromFileName(String fileName) {
-        if (fileName != null) {
-            Matcher m = PATTERN.matcher(fileName);
+    public static BookName fromRepoRelativePath(String repoRelativePath) {
+        if (repoRelativePath != null) {
+            Matcher m = PATTERN.matcher(repoRelativePath);
 
             if (m.find()) {
                 String name = m.group(1);
                 String extension = m.group(2);
 
                 if (extension.equals("org")) {
-                    return new BookName(fileName, name, BookFormat.ORG);
+                    return new BookName(repoRelativePath, name, BookFormat.ORG);
                 }
             }
         }
 
-        throw new IllegalArgumentException("Unsupported book file name " + fileName);
+        throw new IllegalArgumentException("Unsupported book file name " + repoRelativePath);
     }
 
     public String getName() {
@@ -120,8 +133,8 @@ public BookFormat getFormat() {
         return mFormat;
     }
 
-    public String getFileName() {
-        return mFileName;
+    public String getRepoRelativePath() {
+        return mRepoRelativePath;
     }
 
 }
diff --git a/app/src/main/java/com/orgzly/android/LocalStorage.java b/app/src/main/java/com/orgzly/android/LocalStorage.java
index 360237600..c3f476753 100644
--- a/app/src/main/java/com/orgzly/android/LocalStorage.java
+++ b/app/src/main/java/com/orgzly/android/LocalStorage.java
@@ -34,7 +34,7 @@ public LocalStorage(Context context) {
      * @throws IOException if external directory is not available
      */
     public File getExportFile(String name, BookFormat format) throws IOException {
-        return new File(downloadsDirectory(), BookName.fileName(name, format));
+        return new File(downloadsDirectory(), BookName.repoRelativePath(name, format));
     }
 
     /**
diff --git a/app/src/main/java/com/orgzly/android/data/DataRepository.kt b/app/src/main/java/com/orgzly/android/data/DataRepository.kt
index 0861b2007..3b93329b0 100644
--- a/app/src/main/java/com/orgzly/android/data/DataRepository.kt
+++ b/app/src/main/java/com/orgzly/android/data/DataRepository.kt
@@ -80,9 +80,9 @@ class DataRepository @Inject constructor(
                     BookAction.Type.PROGRESS,
                     resources.getString(R.string.force_loading_from_uri, book.linkRepo.url)))
 
-            val fileName = BookName.getFileName(book)
+            val repoRelativePath = BookName.getRepoRelativePath(book)
 
-            val loadedBook = loadBookFromRepo(book.linkRepo.id, book.linkRepo.type, book.linkRepo.url, fileName)
+            val loadedBook = loadBookFromRepo(book.linkRepo.id, book.linkRepo.type, book.linkRepo.url, repoRelativePath)
 
             setBookLastActionAndSyncStatus(loadedBook!!.book.id, BookAction.forNow(
                     BookAction.Type.INFO,
@@ -103,7 +103,7 @@ class DataRepository @Inject constructor(
         val book = getBookView(bookId)
                 ?: throw IOException(resources.getString(R.string.book_does_not_exist_anymore))
 
-        val repositoryPath: String = BookName.getFileName(book)
+        val repositoryPath: String = BookName.getRepoRelativePath(book)
 
         try {
             /* Prefer link. */
@@ -386,7 +386,7 @@ class DataRepository @Inject constructor(
 
             /* Do not rename if the new filename will be ignored */
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
-                RepoUtils.ensureFileNameIsNotIgnored(repo, BookName.fileName(name, BookFormat.ORG))
+                RepoUtils.ensurePathIsNotIgnored(repo, BookName.repoRelativePath(name, BookFormat.ORG))
 
             val movedVrook = repo.renameBook(vrook.uri, name)
 
@@ -513,8 +513,8 @@ class DataRepository @Inject constructor(
             // Ensure that the resulting file name is not ignored in this repo
             val syncRepo = getRepoInstance(repo.id, repo.type, repo.url)
             val bookName = getBook(bookId)!!.name
-            val fileName = BookName.fileName(bookName, BookFormat.ORG)
-            RepoUtils.ensureFileNameIsNotIgnored(syncRepo, fileName)
+            val repoRelativePath = BookName.repoRelativePath(bookName, BookFormat.ORG)
+            RepoUtils.ensurePathIsNotIgnored(syncRepo, repoRelativePath)
         }
 
         db.bookLink().upsert(bookId, repoId)
@@ -1625,13 +1625,13 @@ class DataRepository @Inject constructor(
 
     @Throws(IOException::class)
     fun loadBookFromRepo(rook: Rook): BookView? {
-        val fileName = BookName.getFileName(rook.repoUri, rook.uri)
+        val repoRelativePath = BookName.getRepoRelativePath(rook.repoUri, rook.uri)
 
-        return loadBookFromRepo(rook.repoId, rook.repoType, rook.repoUri.toString(), fileName)
+        return loadBookFromRepo(rook.repoId, rook.repoType, rook.repoUri.toString(), repoRelativePath)
     }
 
     @Throws(IOException::class)
-    fun loadBookFromRepo(repoId: Long, repoType: RepoType, repoUrl: String, fileName: String): BookView? {
+    fun loadBookFromRepo(repoId: Long, repoType: RepoType, repoUrl: String, repoRelativePath: String): BookView? {
         val book: BookView?
 
         val repo = getRepoInstance(repoId, repoType, repoUrl)
@@ -1639,9 +1639,9 @@ class DataRepository @Inject constructor(
         val tmpFile = getTempBookFile()
         try {
             /* Download from repo. */
-            val vrook = repo.retrieveBook(fileName, tmpFile)
+            val vrook = repo.retrieveBook(repoRelativePath, tmpFile)
 
-            val bookName = BookName.fromFileName(fileName)
+            val bookName = BookName.fromRepoRelativePath(repoRelativePath)
 
             /* Store from file to Shelf. */
             book = loadBookFromFile(bookName.name, bookName.format, tmpFile, vrook)
diff --git a/app/src/main/java/com/orgzly/android/git/GitFileSynchronizer.java b/app/src/main/java/com/orgzly/android/git/GitFileSynchronizer.java
index 21c340a60..f626246a6 100644
--- a/app/src/main/java/com/orgzly/android/git/GitFileSynchronizer.java
+++ b/app/src/main/java/com/orgzly/android/git/GitFileSynchronizer.java
@@ -60,11 +60,11 @@ private GitTransportSetter transportSetter() {
 
     public void retrieveLatestVersionOfFile(
             String repositoryPath, File destination) throws IOException {
-        MiscUtils.copyFile(repoDirectoryFile(repositoryPath), destination);
+        MiscUtils.copyFile(workTreeFile(repositoryPath), destination);
     }
 
     public InputStream openRepoFileInputStream(String repositoryPath) throws FileNotFoundException {
-        return new FileInputStream(repoDirectoryFile(repositoryPath));
+        return new FileInputStream(workTreeFile(repositoryPath));
     }
 
     private void fetch() throws IOException {
@@ -119,13 +119,13 @@ private String createMergeBranchName(String repositoryPath, ObjectId commitHash)
     }
 
     public boolean updateAndCommitFileFromRevisionAndMerge(
-            File sourceFile, String repositoryPath,
+            File sourceFile, String repoRelativePath,
             ObjectId fileRevision, RevCommit revision)
             throws IOException {
         ensureRepoIsClean();
-        if (updateAndCommitFileFromRevision(sourceFile, repositoryPath, fileRevision)) {
+        if (updateAndCommitFileFromRevision(sourceFile, repoRelativePath, fileRevision)) {
             if (BuildConfig.LOG_DEBUG) {
-                LogUtils.d(TAG, String.format("File '%s' committed without conflicts.", repositoryPath));
+                LogUtils.d(TAG, String.format("File '%s' committed without conflicts.", repoRelativePath));
             }
             return true;
         }
@@ -134,7 +134,7 @@ public boolean updateAndCommitFileFromRevisionAndMerge(
         if (BuildConfig.LOG_DEBUG) {
             LogUtils.d(TAG, String.format("originalBranch is set to %s", originalBranch));
         }
-        String mergeBranch = createMergeBranchName(repositoryPath, fileRevision);
+        String mergeBranch = createMergeBranchName(repoRelativePath, fileRevision);
         if (BuildConfig.LOG_DEBUG) {
             LogUtils.d(TAG, String.format("originalBranch is set to %s", originalBranch));
             LogUtils.d(TAG, String.format("Temporary mergeBranch is set to %s", mergeBranch));
@@ -157,12 +157,12 @@ public boolean updateAndCommitFileFromRevisionAndMerge(
                     setStartPoint(branchStartPoint).setName(mergeBranch).call();
             if (!currentHead().equals(branchStartPoint))
                 throw new IOException("Failed to create new branch at " + branchStartPoint.toString());
-            if (!updateAndCommitFileFromRevision(sourceFile, repositoryPath, fileRevision))
+            if (!updateAndCommitFileFromRevision(sourceFile, repoRelativePath, fileRevision))
                 throw new IOException(
                         String.format(
                                 "The provided file revision %s for %s is " +
                                         "not the same as the one found in the provided commit %s.",
-                                fileRevision.toString(), repositoryPath, revision.toString()));
+                                fileRevision.toString(), repoRelativePath, revision.toString()));
             mergeSucceeded = doMerge(mergeTarget);
             if (mergeSucceeded) {
                 RevCommit merged = currentHead();
@@ -287,11 +287,11 @@ private void gitResetMerge() throws IOException, GitAPIException {
     }
 
     public boolean updateAndCommitFileFromRevision(
-            File sourceFile, String repositoryPath, ObjectId revision) throws IOException {
+            File sourceFile, String repoRelativePath, ObjectId revision) throws IOException {
         ensureRepoIsClean();
-        ObjectId repositoryRevision = getFileRevision(repositoryPath, currentHead());
+        ObjectId repositoryRevision = getFileRevision(repoRelativePath, currentHead());
         if (repositoryRevision.equals(revision)) {
-            updateAndCommitFile(sourceFile, repositoryPath);
+            updateAndCommitFile(sourceFile, repoRelativePath);
             return true;
         }
         return false;
@@ -377,7 +377,7 @@ public boolean attemptReturnToMainBranch() throws IOException {
 
     public void updateAndCommitExistingFile(File sourceFile, String repositoryPath) throws IOException {
         ensureRepoIsClean();
-        File destinationFile = repoDirectoryFile(repositoryPath);
+        File destinationFile = workTreeFile(repositoryPath);
         if (!destinationFile.exists()) {
             throw new FileNotFoundException("File " + destinationFile + " does not exist");
         }
@@ -392,7 +392,7 @@ public void updateAndCommitExistingFile(File sourceFile, String repositoryPath)
      */
     public void addAndCommitNewFile(File sourceFile, String repositoryPath) throws IOException {
         ensureRepoIsClean();
-        File destinationFile = repoDirectoryFile(repositoryPath);
+        File destinationFile = workTreeFile(repositoryPath);
         if (destinationFile.exists()) {
             throw new IOException("Can't add new file " + repositoryPath + " that already exists.");
         }
@@ -402,7 +402,7 @@ public void addAndCommitNewFile(File sourceFile, String repositoryPath) throws I
 
     private void ensureDirectoryHierarchy(String repositoryPath) throws IOException {
         if (repositoryPath.contains("/")) {
-            File targetDir = repoDirectoryFile(repositoryPath).getParentFile();
+            File targetDir = workTreeFile(repositoryPath).getParentFile();
             if (!(targetDir.exists() || targetDir.mkdirs())) {
                 throw new IOException("The directory " + targetDir.getAbsolutePath() + " could " +
                         "not be created");
@@ -411,13 +411,13 @@ private void ensureDirectoryHierarchy(String repositoryPath) throws IOException
     }
 
     private void updateAndCommitFile(
-            File sourceFile, String fileName) throws IOException {
-        File destinationFile = repoDirectoryFile(fileName);
+            File sourceFile, String repoRelativePath) throws IOException {
+        File destinationFile = workTreeFile(repoRelativePath);
         MiscUtils.copyFile(sourceFile, destinationFile);
         try {
-            git.add().addFilepattern(fileName).call();
+            git.add().addFilepattern(repoRelativePath).call();
             if (!gitRepoIsClean())
-                commit(String.format("Orgzly update: %s", fileName));
+                commit(String.format("Orgzly update: %s", repoRelativePath));
         } catch (GitAPIException e) {
             throw new IOException("Failed to commit changes.");
         }
@@ -443,11 +443,11 @@ public RevCommit getCommit(String identifier) throws IOException {
     }
 
     public RevCommit getLastCommitOfFile(Uri uri) throws GitAPIException {
-        String fileName = uri.toString().replaceFirst("^/", "");
-        return git.log().setMaxCount(1).addPath(fileName).call().iterator().next();
+        String repoRelativePath = uri.toString().replaceFirst("^/", "");
+        return git.log().setMaxCount(1).addPath(repoRelativePath).call().iterator().next();
     }
 
-    public String repoPath() {
+    public String workTreePath() {
         return git.getRepository().getWorkTree().getAbsolutePath();
     }
 
@@ -465,8 +465,8 @@ private void ensureRepoIsClean() throws IOException {
             throw new IOException("Refusing to update because there are uncommitted changes.");
     }
 
-    public File repoDirectoryFile(String filePath) {
-        return new File(repoPath(), filePath);
+    public File workTreeFile(String filePath) {
+        return new File(workTreePath(), filePath);
     }
 
     public boolean isEmptyRepo() throws IOException{
@@ -480,38 +480,38 @@ public ObjectId getFileRevision(String pathString, RevCommit commit) throws IOEx
 
     public boolean deleteFileFromRepo(Uri uri) throws IOException {
         if (mergeWithRemote()) {
-            String fileName = uri.toString().replaceFirst("^/", "");
+            String repoRelativePath = uri.toString().replaceFirst("^/", "");
             try {
-                git.rm().addFilepattern(fileName).call();
+                git.rm().addFilepattern(repoRelativePath).call();
                 if (!gitRepoIsClean())
-                    commit(String.format("Orgzly deletion: %s", fileName));
+                    commit(String.format("Orgzly deletion: %s", repoRelativePath));
                 return true;
             } catch (GitAPIException e) {
-                throw new IOException(String.format("Failed to commit deletion of %s, %s", fileName, e.getMessage()));
+                throw new IOException(String.format("Failed to commit deletion of %s, %s", repoRelativePath, e.getMessage()));
             }
         } else {
             return false;
         }
     }
 
-    public boolean renameFileInRepo(String oldFileName, String newFileName) throws IOException {
+    public boolean renameFileInRepo(String oldPath, String newPath) throws IOException {
         ensureRepoIsClean();
         if (mergeWithRemote()) {
-            File oldFile = repoDirectoryFile(oldFileName);
-            File newFile = repoDirectoryFile(newFileName);
+            File oldFile = workTreeFile(oldPath);
+            File newFile = workTreeFile(newPath);
             // Abort if destination file exists
             if (newFile.exists()) {
-                throw new IOException("Repository file " + newFileName + " already exists.");
+                throw new IOException("Repository file " + newPath + " already exists.");
             }
-            ensureDirectoryHierarchy(newFileName);
+            ensureDirectoryHierarchy(newPath);
             // Copy the file contents and add it to the index
             MiscUtils.copyFile(oldFile, newFile);
             try {
-                git.add().addFilepattern(newFileName).call();
+                git.add().addFilepattern(newPath).call();
                 if (!gitRepoIsClean()) {
                     // Remove the old file from the Git index
-                    git.rm().addFilepattern(oldFileName).call();
-                    commit(String.format("Orgzly: rename %s to %s", oldFileName, newFileName));
+                    git.rm().addFilepattern(oldPath).call();
+                    commit(String.format("Orgzly: rename %s to %s", oldPath, newPath));
                     return true;
                 }
             } catch (GitAPIException e) {
diff --git a/app/src/main/java/com/orgzly/android/repos/DatabaseRepo.java b/app/src/main/java/com/orgzly/android/repos/DatabaseRepo.java
index 10b81b958..a4f575f28 100644
--- a/app/src/main/java/com/orgzly/android/repos/DatabaseRepo.java
+++ b/app/src/main/java/com/orgzly/android/repos/DatabaseRepo.java
@@ -48,24 +48,24 @@ public List<VersionedRook> getBooks() {
     }
 
     @Override
-    public VersionedRook retrieveBook(String fileName, File file) {
-        Uri uri = repoUri.buildUpon().appendPath(fileName).build();
+    public VersionedRook retrieveBook(String repoRelativePath, File file) {
+        Uri uri = repoUri.buildUpon().appendPath(repoRelativePath).build();
         return dbRepo.retrieveBook(repoId, repoUri, uri, file);
     }
 
     @Override
-    public InputStream openRepoFileInputStream(String fileName) throws IOException {
+    public InputStream openRepoFileInputStream(String repoRelativePath) throws IOException {
         throw new UnsupportedOperationException("Not implemented");
     }
 
     @Override
-    public VersionedRook storeBook(File file, String fileName) throws IOException {
+    public VersionedRook storeBook(File file, String repoRelativePath) throws IOException {
         String content = MiscUtils.readStringFromFile(file);
 
         String rev = "MockedRevision-" + System.currentTimeMillis();
         long mtime = System.currentTimeMillis();
 
-        Uri uri = repoUri.buildUpon().appendPath(fileName).build();
+        Uri uri = repoUri.buildUpon().appendPath(repoRelativePath).build();
 
         VersionedRook vrook = new VersionedRook(repoId, RepoType.MOCK, repoUri, uri, rev, mtime);
 
diff --git a/app/src/main/java/com/orgzly/android/repos/DirectoryRepo.java b/app/src/main/java/com/orgzly/android/repos/DirectoryRepo.java
index c2be91cbc..aaaddca22 100644
--- a/app/src/main/java/com/orgzly/android/repos/DirectoryRepo.java
+++ b/app/src/main/java/com/orgzly/android/repos/DirectoryRepo.java
@@ -124,8 +124,8 @@ public List<VersionedRook> getBooks() {
     }
 
     @Override
-    public VersionedRook retrieveBook(String fileName, File destinationFile) throws IOException {
-        Uri uri = repoUri.buildUpon().appendPath(fileName).build();
+    public VersionedRook retrieveBook(String repoRelativePath, File destinationFile) throws IOException {
+        Uri uri = repoUri.buildUpon().appendPath(repoRelativePath).build();
 
         String path = uri.getPath();
 
@@ -145,17 +145,17 @@ public VersionedRook retrieveBook(String fileName, File destinationFile) throws
     }
 
     @Override
-    public InputStream openRepoFileInputStream(String fileName) throws IOException {
-        return new FileInputStream(repoUri.buildUpon().appendPath(fileName).build().getPath());
+    public InputStream openRepoFileInputStream(String repoRelativePath) throws IOException {
+        return new FileInputStream(repoUri.buildUpon().appendPath(repoRelativePath).build().getPath());
     }
 
     @Override
-    public VersionedRook storeBook(File file, String fileName) throws IOException {
+    public VersionedRook storeBook(File file, String repoRelativePath) throws IOException {
         if (!file.exists()) {
             throw new FileNotFoundException("File " + file + " does not exist");
         }
 
-        File destinationFile = new File(mDirectory, fileName);
+        File destinationFile = new File(mDirectory, repoRelativePath);
 
         File destinationFileParent = destinationFile.getParentFile();
 
@@ -172,7 +172,7 @@ public VersionedRook storeBook(File file, String fileName) throws IOException {
         String rev = String.valueOf(destinationFile.lastModified());
         long mtime = destinationFile.lastModified();
 
-        Uri uri = repoUri.buildUpon().appendPath(fileName).build();
+        Uri uri = repoUri.buildUpon().appendPath(repoRelativePath).build();
 
         return new VersionedRook(repoId, RepoType.DIRECTORY, repoUri, uri, rev, mtime);
     }
diff --git a/app/src/main/java/com/orgzly/android/repos/DocumentRepo.java b/app/src/main/java/com/orgzly/android/repos/DocumentRepo.java
index 1062d1bd7..03dc61eca 100644
--- a/app/src/main/java/com/orgzly/android/repos/DocumentRepo.java
+++ b/app/src/main/java/com/orgzly/android/repos/DocumentRepo.java
@@ -115,17 +115,17 @@ private List<DocumentFile> walkFileTree() {
         while (!directoryNodes.isEmpty()) {
             DocumentFile currentDir = directoryNodes.remove(0);
             for (DocumentFile node : currentDir.listFiles()) {
-                String relativeFileName = BookName.getFileName(repoUri, node.getUri());
+                String repoRelativePath = BookName.getRepoRelativePath(repoUri, node.getUri());
                 if (node.isDirectory()) {
                     if (Build.VERSION.SDK_INT >= 26) {
-                        if (ignores.isPathIgnored(relativeFileName, true)) {
+                        if (ignores.isPathIgnored(repoRelativePath, true)) {
                             continue;
                         }
                     }
                     directoryNodes.add(node);
                 } else {
                     if (Build.VERSION.SDK_INT >= 26) {
-                        if (ignores.isPathIgnored(relativeFileName, false)) {
+                        if (ignores.isPathIgnored(repoRelativePath, false)) {
                             continue;
                         }
                     } result.add(node);
@@ -135,19 +135,19 @@ private List<DocumentFile> walkFileTree() {
         return result;
     }
 
-    private DocumentFile getDocumentFileFromFileName(String fileName) {
-        String fullUri = repoDocumentFile.getUri() + Uri.encode("/" + fileName);
+    private DocumentFile getDocumentFileFromPath(String path) {
+        String fullUri = repoDocumentFile.getUri() + Uri.encode("/" + path);
         return DocumentFile.fromSingleUri(context, Uri.parse(fullUri));
     }
 
     @Override
-    public VersionedRook retrieveBook(String fileName, File destinationFile) throws IOException {
-        DocumentFile sourceFile = getDocumentFileFromFileName(fileName);
+    public VersionedRook retrieveBook(String repoRelativePath, File destinationFile) throws IOException {
+        DocumentFile sourceFile = getDocumentFileFromPath(repoRelativePath);
         if (sourceFile == null) {
-            throw new FileNotFoundException("Book " + fileName + " not found in " + repoUri);
+            throw new FileNotFoundException("Book " + repoRelativePath + " not found in " + repoUri);
         } else {
             if (BuildConfig.LOG_DEBUG) {
-                LogUtils.d(TAG, "Found DocumentFile for " + fileName + ": " + sourceFile.getUri());
+                LogUtils.d(TAG, "Found DocumentFile for " + repoRelativePath + ": " + sourceFile.getUri());
             }
         }
 
@@ -163,27 +163,27 @@ public VersionedRook retrieveBook(String fileName, File destinationFile) throws
     }
 
     @Override
-    public InputStream openRepoFileInputStream(String fileName) throws IOException {
-        DocumentFile sourceFile = getDocumentFileFromFileName(fileName);
+    public InputStream openRepoFileInputStream(String repoRelativePath) throws IOException {
+        DocumentFile sourceFile = getDocumentFileFromPath(repoRelativePath);
         if (!sourceFile.exists()) throw new FileNotFoundException();
         return context.getContentResolver().openInputStream(sourceFile.getUri());
     }
 
     @Override
-    public VersionedRook storeBook(File file, String path) throws IOException {
+    public VersionedRook storeBook(File file, String repoRelativePath) throws IOException {
         if (!file.exists()) {
             throw new FileNotFoundException("File " + file + " does not exist");
         }
-        DocumentFile destinationFile = getDocumentFileFromFileName(path);
-        if (path.contains("/")) {
-            DocumentFile destinationDir = ensureDirectoryHierarchy(path);
-            String fileName = Uri.parse(path).getLastPathSegment();
+        DocumentFile destinationFile = getDocumentFileFromPath(repoRelativePath);
+        if (repoRelativePath.contains("/")) {
+            DocumentFile destinationDir = ensureDirectoryHierarchy(repoRelativePath);
+            String fileName = Uri.parse(repoRelativePath).getLastPathSegment();
             if (destinationDir.findFile(fileName) == null) {
                 destinationFile = destinationDir.createFile("text/*", fileName);
             }
         } else {
             if (!destinationFile.exists()) {
-                repoDocumentFile.createFile("text/*", path);
+                repoDocumentFile.createFile("text/*", repoRelativePath);
             }
         }
 
@@ -239,8 +239,8 @@ public VersionedRook renameBook(Uri oldFullUri, String newName) throws IOExcepti
                         ""
                 )
         );
-        BookName oldBookName = BookName.fromFileName(BookName.getFileName(repoUri, oldFullUri));
-        String newRelativePath = BookName.fileName(newName, oldBookName.getFormat());
+        BookName oldBookName = BookName.fromRepoRelativePath(BookName.getRepoRelativePath(repoUri, oldFullUri));
+        String newRelativePath = BookName.repoRelativePath(newName, oldBookName.getFormat());
         String newDocFileName = Uri.parse(newRelativePath).getLastPathSegment();
         DocumentFile newDir;
         Uri newUri = oldFullUri;
diff --git a/app/src/main/java/com/orgzly/android/repos/DropboxClient.java b/app/src/main/java/com/orgzly/android/repos/DropboxClient.java
index 745384b02..fceec62ee 100644
--- a/app/src/main/java/com/orgzly/android/repos/DropboxClient.java
+++ b/app/src/main/java/com/orgzly/android/repos/DropboxClient.java
@@ -12,6 +12,7 @@
 import com.dropbox.core.json.JsonReadException;
 import com.dropbox.core.oauth.DbxCredential;
 import com.dropbox.core.v2.DbxClientV2;
+import com.dropbox.core.v2.files.DeleteResult;
 import com.dropbox.core.v2.files.FileMetadata;
 import com.dropbox.core.v2.files.FolderMetadata;
 import com.dropbox.core.v2.files.GetMetadataErrorException;
@@ -219,18 +220,18 @@ public List<VersionedRook> getBooks(Uri repoUri, RepoIgnoreNode ignores) throws
         return list;
     }
 
-    private Uri getFullUriFromRelativePath(Uri repoUri, String relativePath) {
-        String encodedFileName = Uri.encode(relativePath, "/");
-        return Uri.withAppendedPath(repoUri, encodedFileName);
+    private Uri getFullUriFromRelativePath(Uri repoUri, String repoRelativePath) {
+        String encodedPath = Uri.encode(repoRelativePath, "/");
+        return Uri.withAppendedPath(repoUri, encodedPath);
     }
 
     /**
      * Download file from Dropbox and store it to a local file.
      */
-    public VersionedRook download(Uri repoUri, String relativePath, File localFile) throws IOException {
+    public VersionedRook download(Uri repoUri, String repoRelativePath, File localFile) throws IOException {
         linkedOrThrow();
 
-        Uri uri = getFullUriFromRelativePath(repoUri, relativePath);
+        Uri uri = getFullUriFromRelativePath(repoUri, repoRelativePath);
 
         OutputStream out = new BufferedOutputStream(new FileOutputStream(localFile));
 
@@ -262,10 +263,10 @@ public VersionedRook download(Uri repoUri, String relativePath, File localFile)
         }
     }
 
-    public InputStream streamFile(Uri repoUri, String fileName) throws IOException {
+    public InputStream streamFile(Uri repoUri, String repoRelativePath) throws IOException {
         linkedOrThrow();
 
-        Uri uri = repoUri.buildUpon().appendPath(fileName).build();
+        Uri uri = repoUri.buildUpon().appendPath(repoRelativePath).build();
         FileMetadata metadata;
         String rev;
         DbxDownloader<FileMetadata> downloader;
diff --git a/app/src/main/java/com/orgzly/android/repos/DropboxRepo.java b/app/src/main/java/com/orgzly/android/repos/DropboxRepo.java
index 1dbe611ab..21cfdf65c 100644
--- a/app/src/main/java/com/orgzly/android/repos/DropboxRepo.java
+++ b/app/src/main/java/com/orgzly/android/repos/DropboxRepo.java
@@ -45,24 +45,24 @@ public List<VersionedRook> getBooks() throws IOException {
     }
 
     @Override
-    public VersionedRook retrieveBook(String fileName, File file) throws IOException {
-        return client.download(repoUri, fileName, file);
+    public VersionedRook retrieveBook(String repoRelativePath, File file) throws IOException {
+        return client.download(repoUri, repoRelativePath, file);
     }
 
     @Override
-    public InputStream openRepoFileInputStream(String fileName) throws IOException {
-        return client.streamFile(repoUri, fileName);
+    public InputStream openRepoFileInputStream(String repoRelativePath) throws IOException {
+        return client.streamFile(repoUri, repoRelativePath);
     }
 
     @Override
-    public VersionedRook storeBook(File file, String fileName) throws IOException {
-        return client.upload(file, repoUri, fileName);
+    public VersionedRook storeBook(File file, String repoRelativePath) throws IOException {
+        return client.upload(file, repoUri, repoRelativePath);
     }
 
     @Override
     public VersionedRook renameBook(Uri oldFullUri, String newName) throws IOException {
-        BookName oldBookName = BookName.fromFileName(BookName.getFileName(repoUri, oldFullUri));
-        String newRelativePath = BookName.fileName(newName, oldBookName.getFormat());
+        BookName oldBookName = BookName.fromRepoRelativePath(BookName.getRepoRelativePath(repoUri, oldFullUri));
+        String newRelativePath = BookName.repoRelativePath(newName, oldBookName.getFormat());
         String newEncodedRelativePath = Uri.encode(newRelativePath, "/");
         Uri newFullUri = repoUri.buildUpon().appendEncodedPath(newEncodedRelativePath).build();
         return client.move(repoUri, oldFullUri, newFullUri);
@@ -74,7 +74,7 @@ public void delete(Uri uri) throws IOException {
     }
 
     /**
-     * Intended for tests. The delete() method does not allow deleting directories.
+     * Only used by tests. The delete() method does not allow deleting directories.
      */
     public void deleteDirectory(Uri uri) throws IOException {
         client.deleteFolder(uri.getPath());
diff --git a/app/src/main/java/com/orgzly/android/repos/GitRepo.java b/app/src/main/java/com/orgzly/android/repos/GitRepo.java
index 1e0b6bddf..2cf1941e1 100644
--- a/app/src/main/java/com/orgzly/android/repos/GitRepo.java
+++ b/app/src/main/java/com/orgzly/android/repos/GitRepo.java
@@ -185,16 +185,16 @@ public boolean isAutoSyncSupported() {
         return true;
     }
 
-    public VersionedRook storeBook(File file, String repositoryPath) throws IOException {
-        File destination = synchronizer.repoDirectoryFile(repositoryPath);
+    public VersionedRook storeBook(File file, String repoRelativePath) throws IOException {
+        File destination = synchronizer.workTreeFile(repoRelativePath);
 
         if (destination.exists()) {
-            synchronizer.updateAndCommitExistingFile(file, repositoryPath);
+            synchronizer.updateAndCommitExistingFile(file, repoRelativePath);
         } else {
-            synchronizer.addAndCommitNewFile(file, repositoryPath);
+            synchronizer.addAndCommitNewFile(file, repoRelativePath);
         }
         synchronizer.tryPush();
-        return currentVersionedRook(Uri.EMPTY.buildUpon().appendPath(repositoryPath).build());
+        return currentVersionedRook(Uri.EMPTY.buildUpon().appendPath(repoRelativePath).build());
     }
 
     private RevWalk walk() {
@@ -206,9 +206,9 @@ RevCommit getCommitFromRevisionString(String revisionString) throws IOException
     }
 
     @Override
-    public VersionedRook retrieveBook(String fileName, File destination) throws IOException {
+    public VersionedRook retrieveBook(String repoRelativePath, File destination) throws IOException {
 
-        Uri sourceUri = Uri.parse("/" + fileName);
+        Uri sourceUri = Uri.parse("/" + repoRelativePath);
 
         // Ensure our repo copy is up-to-date. This is necessary when force-loading a book.
         synchronizer.mergeWithRemote();
@@ -219,8 +219,8 @@ public VersionedRook retrieveBook(String fileName, File destination) throws IOEx
     }
 
     @Override
-    public InputStream openRepoFileInputStream(String fileName) throws IOException {
-        Uri sourceUri = Uri.parse(fileName);
+    public InputStream openRepoFileInputStream(String repoRelativePath) throws IOException {
+        Uri sourceUri = Uri.parse(repoRelativePath);
         return synchronizer.openRepoFileInputStream(sourceUri.getPath());
     }
 
@@ -265,12 +265,12 @@ public List<VersionedRook> getBooks() throws IOException {
             public boolean include(TreeWalk walker) {
                 final FileMode mode = walk.getFileMode();
                 final boolean isDirectory = mode == FileMode.TREE;
-                final String filePath = walk.getPathString();
-                if (ignores.isIgnored(filePath, isDirectory) == IgnoreNode.MatchResult.IGNORED)
+                final String repoRelativePath = walk.getPathString();
+                if (ignores.isIgnored(repoRelativePath, isDirectory) == IgnoreNode.MatchResult.IGNORED)
                     return false;
                 if (isDirectory)
                     return true;
-                return BookName.isSupportedFormatFileName(filePath);
+                return BookName.isSupportedFormatFileName(repoRelativePath);
             }
 
             @Override
@@ -298,11 +298,11 @@ public void delete(Uri uri) throws IOException {
     }
 
     public VersionedRook renameBook(Uri oldFullUri, String newName) throws IOException {
-        String oldFileName = oldFullUri.toString().replaceFirst("^/", "");
-        String newFileName = BookName.fileName(newName, BookFormat.ORG);
-        if (synchronizer.renameFileInRepo(oldFileName, newFileName)) {
+        String oldPath = oldFullUri.toString().replaceFirst("^/", "");
+        String newPath = BookName.repoRelativePath(newName, BookFormat.ORG);
+        if (synchronizer.renameFileInRepo(oldPath, newPath)) {
             synchronizer.tryPush();
-            return currentVersionedRook(Uri.EMPTY.buildUpon().appendPath(newFileName).build());
+            return currentVersionedRook(Uri.EMPTY.buildUpon().appendPath(newPath).build());
         } else {
             return null;
         }
@@ -311,16 +311,16 @@ public VersionedRook renameBook(Uri oldFullUri, String newName) throws IOExcepti
     @Override
     public TwoWaySyncResult syncBook(
             Uri uri, VersionedRook current, File fromDB) throws IOException {
-        String fileName = uri.getPath().replaceFirst("^/", "");
+        String repoRelativePath = uri.getPath().replaceFirst("^/", "");
         boolean merged = true;
         if (current != null) {
             RevCommit rookCommit = getCommitFromRevisionString(current.getRevision());
             if (BuildConfig.LOG_DEBUG) {
-                LogUtils.d(TAG, String.format("Syncing file %s, rookCommit: %s", fileName, rookCommit));
+                LogUtils.d(TAG, String.format("Syncing file %s, rookCommit: %s", repoRelativePath, rookCommit));
             }
             merged = synchronizer.updateAndCommitFileFromRevisionAndMerge(
-                    fromDB, fileName,
-                    synchronizer.getFileRevision(fileName, rookCommit),
+                    fromDB, repoRelativePath,
+                    synchronizer.getFileRevision(repoRelativePath, rookCommit),
                     rookCommit);
 
             if (merged) {
@@ -333,9 +333,9 @@ public TwoWaySyncResult syncBook(
         } else {
             Log.w(TAG, "Unable to find previous commit, loading from repository.");
         }
-        File writeBackFile = synchronizer.repoDirectoryFile(fileName);
+        File writeBackFile = synchronizer.workTreeFile(repoRelativePath);
         return new TwoWaySyncResult(
-                currentVersionedRook(Uri.EMPTY.buildUpon().appendPath(fileName).build()), merged,
+                currentVersionedRook(Uri.EMPTY.buildUpon().appendPath(repoRelativePath).build()), merged,
                 writeBackFile);
     }
 
diff --git a/app/src/main/java/com/orgzly/android/repos/MockRepo.java b/app/src/main/java/com/orgzly/android/repos/MockRepo.java
index 35f8b9cea..df2bedbb0 100644
--- a/app/src/main/java/com/orgzly/android/repos/MockRepo.java
+++ b/app/src/main/java/com/orgzly/android/repos/MockRepo.java
@@ -4,9 +4,14 @@
 import android.os.SystemClock;
 
 
+import androidx.test.core.app.ApplicationProvider;
+
 import com.orgzly.android.data.DbRepoBookRepository;
+import com.orgzly.android.prefs.AppPreferences;
 
+import java.io.ByteArrayInputStream;
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
@@ -25,10 +30,16 @@ public class MockRepo implements SyncRepo {
     private static final long SLEEP_FOR_STORE_BOOK = 200;
     private static final long SLEEP_FOR_DELETE_BOOK = 100;
 
+    public static final String IGNORE_RULES_PREF_KEY = "ignore_rules";
+
+    private String ignoreRules;
+
     private DatabaseRepo databaseRepo;
 
     public MockRepo(RepoWithProps repoWithProps, DbRepoBookRepository dbRepo) {
         databaseRepo = new DatabaseRepo(repoWithProps, dbRepo);
+        ignoreRules = AppPreferences.repoPropsMap(ApplicationProvider.getApplicationContext(),
+                repoWithProps.getRepo().getId()).get(IGNORE_RULES_PREF_KEY);
     }
 
     @Override
@@ -53,20 +64,24 @@ public List<VersionedRook> getBooks() throws IOException {
     }
 
     @Override
-    public VersionedRook retrieveBook(String fileName, File file) throws IOException {
+    public VersionedRook retrieveBook(String repoRelativePath, File file) throws IOException {
         SystemClock.sleep(SLEEP_FOR_RETRIEVE_BOOK);
-        return databaseRepo.retrieveBook(fileName, file);
+        return databaseRepo.retrieveBook(repoRelativePath, file);
     }
 
     @Override
-    public InputStream openRepoFileInputStream(String fileName) throws IOException {
-        throw new FileNotFoundException();
+    public InputStream openRepoFileInputStream(String repoRelativePath) throws IOException {
+        if (repoRelativePath.equals(RepoIgnoreNode.IGNORE_FILE) && ignoreRules != null) {
+            return new ByteArrayInputStream(ignoreRules.getBytes());
+        } else {
+            throw new FileNotFoundException();
+        }
     }
 
     @Override
-    public VersionedRook storeBook(File file, String fileName) throws IOException {
+    public VersionedRook storeBook(File file, String repoRelativePath) throws IOException {
         SystemClock.sleep(SLEEP_FOR_STORE_BOOK);
-        return databaseRepo.storeBook(file, fileName);
+        return databaseRepo.storeBook(file, repoRelativePath);
     }
 
     @Override
diff --git a/app/src/main/java/com/orgzly/android/repos/RepoIgnoreNode.kt b/app/src/main/java/com/orgzly/android/repos/RepoIgnoreNode.kt
index d8fe16411..25e7826b8 100644
--- a/app/src/main/java/com/orgzly/android/repos/RepoIgnoreNode.kt
+++ b/app/src/main/java/com/orgzly/android/repos/RepoIgnoreNode.kt
@@ -44,7 +44,7 @@ class RepoIgnoreNode(repo: SyncRepo) : IgnoreNode() {
     }
 
     @RequiresApi(Build.VERSION_CODES.O)
-    fun ensureFileNameIsNotIgnored(filePath: String) {
+    fun ensurePathIsNotIgnored(filePath: String) {
         if (isPathIgnored(filePath, false)) {
             throw IOException(
                 App.getAppContext().getString(
diff --git a/app/src/main/java/com/orgzly/android/repos/RepoUtils.java b/app/src/main/java/com/orgzly/android/repos/RepoUtils.java
index 639e77cc2..9c4f2942d 100644
--- a/app/src/main/java/com/orgzly/android/repos/RepoUtils.java
+++ b/app/src/main/java/com/orgzly/android/repos/RepoUtils.java
@@ -32,8 +32,8 @@ public static boolean isAutoSyncSupported(Collection<SyncRepo> repos) {
     }
 
     @RequiresApi(api = Build.VERSION_CODES.O)
-    public static void ensureFileNameIsNotIgnored(SyncRepo repo, String fileName) {
-        new RepoIgnoreNode(repo).ensureFileNameIsNotIgnored(fileName);
+    public static void ensurePathIsNotIgnored(SyncRepo repo, String repoRelativePath) {
+        new RepoIgnoreNode(repo).ensurePathIsNotIgnored(repoRelativePath);
     }
 }
 
diff --git a/app/src/main/java/com/orgzly/android/repos/SyncRepo.java b/app/src/main/java/com/orgzly/android/repos/SyncRepo.java
index 6009051a6..11d6cfb33 100644
--- a/app/src/main/java/com/orgzly/android/repos/SyncRepo.java
+++ b/app/src/main/java/com/orgzly/android/repos/SyncRepo.java
@@ -31,23 +31,31 @@ public interface SyncRepo {
     /**
      * Download the latest available revision of the book and store its content to {@code File}.
      */
-    VersionedRook retrieveBook(String fileName, File destination) throws IOException;
+    VersionedRook retrieveBook(String repoRelativePath, File destination) throws IOException;
 
     /**
      * Open a file in the repository for reading. Originally added for parsing the .orgzlyignore
      * file.
-     * @param fileName The file to open
+     * @param repoRelativePath The file to open
      * @throws IOException
      */
-    InputStream openRepoFileInputStream(String fileName) throws IOException;
+    InputStream openRepoFileInputStream(String repoRelativePath) throws IOException;
 
     /**
      * Uploads book storing it under given filename under repo's url.
      * @param file The contents of this file should be stored at the remote location/repo
-     * @param fileName The contents ({@code file}) should be stored under this name
+     * @param repoRelativePath The contents ({@code file}) should be stored under this
+     *                       (non-encoded) name
      */
-    VersionedRook storeBook(File file, String fileName) throws IOException;
+    VersionedRook storeBook(File file, String repoRelativePath) throws IOException;
 
+    /**
+     *
+     * @param oldFullUri Uri of the original repository file
+     * @param newName The new desired book name
+     * @return
+     * @throws IOException
+     */
     VersionedRook renameBook(Uri oldFullUri, String newName) throws IOException;
 
     // VersionedRook moveBook(Uri from, Uri uri) throws IOException;
diff --git a/app/src/main/java/com/orgzly/android/repos/WebdavRepo.kt b/app/src/main/java/com/orgzly/android/repos/WebdavRepo.kt
index a1be5f5ba..81599b893 100644
--- a/app/src/main/java/com/orgzly/android/repos/WebdavRepo.kt
+++ b/app/src/main/java/com/orgzly/android/repos/WebdavRepo.kt
@@ -186,8 +186,8 @@ class WebdavRepo(
                 .toMutableList()
     }
 
-    override fun retrieveBook(fileName: String?, destination: File?): VersionedRook {
-        val fileUrl = Uri.withAppendedPath(uri, fileName).toUrl()
+    override fun retrieveBook(repoRelativePath: String?, destination: File?): VersionedRook {
+        val fileUrl = Uri.withAppendedPath(uri, repoRelativePath).toUrl()
 
         sardine.get(fileUrl).use { inputStream ->
             FileOutputStream(destination).use { outputStream ->
@@ -198,8 +198,8 @@ class WebdavRepo(
         return sardine.list(fileUrl).first().toVersionedRook()
     }
 
-    override fun openRepoFileInputStream(fileName: String): InputStream {
-        val fileUrl = Uri.withAppendedPath(uri, fileName).toUrl()
+    override fun openRepoFileInputStream(repoRelativePath: String): InputStream {
+        val fileUrl = Uri.withAppendedPath(uri, repoRelativePath).toUrl()
         if (!sardine.exists(fileUrl))
             throw FileNotFoundException()
         return sardine.get(fileUrl)
@@ -218,14 +218,14 @@ class WebdavRepo(
         }
     }
 
-    override fun storeBook(file: File, fileName: String): VersionedRook {
-        val encodedFileName = Uri.encode(fileName, "/")
-        if (encodedFileName != null) {
-            if (encodedFileName.contains("/")) {
-                ensureDirectoryHierarchy(encodedFileName)
+    override fun storeBook(file: File, repoRelativePath: String): VersionedRook {
+        val encodedRepoPath = Uri.encode(repoRelativePath, "/")
+        if (encodedRepoPath != null) {
+            if (encodedRepoPath.contains("/")) {
+                ensureDirectoryHierarchy(encodedRepoPath)
             }
         }
-        val fileUrl = uri.buildUpon().appendEncodedPath(encodedFileName).build().toUrl()
+        val fileUrl = uri.buildUpon().appendEncodedPath(encodedRepoPath).build().toUrl()
 
         sardine.put(fileUrl, file, null)
 
@@ -233,8 +233,8 @@ class WebdavRepo(
     }
 
     override fun renameBook(oldFullUri: Uri, newName: String): VersionedRook {
-        val oldBookName = BookName.fromFileName(BookName.getFileName(uri, oldFullUri))
-        val newRelativePath = BookName.fileName(newName, oldBookName.format)
+        val oldBookName = BookName.fromRepoRelativePath(BookName.getRepoRelativePath(uri, oldFullUri))
+        val newRelativePath = BookName.repoRelativePath(newName, oldBookName.format)
         val newEncodedRelativePath = Uri.encode(newRelativePath, "/")
         val newFullUrl = uri.buildUpon().appendEncodedPath(newEncodedRelativePath).build().toUrl()
 
diff --git a/app/src/main/java/com/orgzly/android/sync/BookNamesake.java b/app/src/main/java/com/orgzly/android/sync/BookNamesake.java
index eaa4390cc..7d86055f6 100644
--- a/app/src/main/java/com/orgzly/android/sync/BookNamesake.java
+++ b/app/src/main/java/com/orgzly/android/sync/BookNamesake.java
@@ -47,8 +47,8 @@ public static Map<String, BookNamesake> getAll(List<BookView> books, List<Versio
 
         /* Set repo books. */
         for (VersionedRook book: versionedRooks) {
-            String fileName = BookName.getFileName(book.getRepoUri(), book.getUri());
-            String name = BookName.fromFileName(fileName).getName();
+            String repoRelativePath = BookName.getRepoRelativePath(book.getRepoUri(), book.getUri());
+            String name = BookName.fromRepoRelativePath(repoRelativePath).getName();
 
             BookNamesake pair = namesakes.get(name);
             if (pair == null) {
diff --git a/app/src/main/java/com/orgzly/android/sync/SyncUtils.kt b/app/src/main/java/com/orgzly/android/sync/SyncUtils.kt
index f1081adc7..ddf69afe1 100644
--- a/app/src/main/java/com/orgzly/android/sync/SyncUtils.kt
+++ b/app/src/main/java/com/orgzly/android/sync/SyncUtils.kt
@@ -155,7 +155,7 @@ object SyncUtils {
             BookSyncStatus.ONLY_BOOK_WITHOUT_LINK_AND_ONE_REPO -> {
                 repoEntity = dataRepository.getRepos().iterator().next()
                 repoUrl = repoEntity.url
-                repositoryPath = BookName.fileName(namesake.book.book.name, BookFormat.ORG)
+                repositoryPath = BookName.repoRelativePath(namesake.book.book.name, BookFormat.ORG)
                 /* Set repo link before saving to ensure repo ignore rules are checked */
                 dataRepository.setLink(namesake.book.book.id, repoEntity)
                 dataRepository.saveBookToRepo(repoEntity, repositoryPath, namesake.book, BookFormat.ORG)
@@ -165,7 +165,7 @@ object SyncUtils {
             BookSyncStatus.BOOK_WITH_LINK_LOCAL_MODIFIED -> {
                 repoEntity = namesake.book.linkRepo
                 repoUrl = repoEntity!!.url
-                repositoryPath = BookName.getFileName(repoUrl.toUri(), namesake.book.syncedTo!!.uri)
+                repositoryPath = BookName.getRepoRelativePath(repoUrl.toUri(), namesake.book.syncedTo!!.uri)
                 dataRepository.saveBookToRepo(repoEntity, repositoryPath, namesake.book, BookFormat.ORG)
                 bookAction = BookAction.forNow(BookAction.Type.INFO, namesake.status.msg(repoUrl))
             }
@@ -173,7 +173,7 @@ object SyncUtils {
             BookSyncStatus.ONLY_BOOK_WITH_LINK -> {
                 repoEntity = namesake.book.linkRepo
                 repoUrl = repoEntity!!.url
-                repositoryPath = BookName.fileName(namesake.book.book.name, BookFormat.ORG)
+                repositoryPath = BookName.repoRelativePath(namesake.book.book.name, BookFormat.ORG)
                 dataRepository.saveBookToRepo(repoEntity, repositoryPath, namesake.book, BookFormat.ORG)
                 bookAction = BookAction.forNow(BookAction.Type.INFO, namesake.status.msg(repoUrl))
             }
@@ -190,8 +190,8 @@ object SyncUtils {
         var noNewMergeConflicts = true
         // If there are only local changes, the GitRepo.syncBook method is overly complicated.
         if (namesake.status == BookSyncStatus.BOOK_WITH_LINK_LOCAL_MODIFIED) {
-            val fileName = BookName.getFileName(repo.getUri(), namesake.book.syncedTo!!.uri)
-            dataRepository.saveBookToRepo(namesake.book.linkRepo!!, fileName, namesake.book, BookFormat.ORG)
+            val repoRelativePath = BookName.getRepoRelativePath(repo.getUri(), namesake.book.syncedTo!!.uri)
+            dataRepository.saveBookToRepo(namesake.book.linkRepo!!, repoRelativePath, namesake.book, BookFormat.ORG)
         } else {
             val dbFile = dataRepository.getTempBookFile()
             try {
@@ -202,8 +202,8 @@ object SyncUtils {
                 newRook = newRook1
                 // We only need to write it if syncback is needed
                 if (loadFile != null) {
-                    val fileName = BookName.getFileName(repo.getUri(), newRook.uri)
-                    val bookName = BookName.fromFileName(fileName)
+                    val repoRelativePath = BookName.getRepoRelativePath(repo.getUri(), newRook.uri)
+                    val bookName = BookName.fromRepoRelativePath(repoRelativePath)
                     if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, "Loading from file '$loadFile'")
                     dataRepository.loadBookFromFile(
                         bookName.name,
diff --git a/app/src/main/java/com/orgzly/android/ui/books/BooksFragment.kt b/app/src/main/java/com/orgzly/android/ui/books/BooksFragment.kt
index b025568dd..68c4f91eb 100644
--- a/app/src/main/java/com/orgzly/android/ui/books/BooksFragment.kt
+++ b/app/src/main/java/com/orgzly/android/ui/books/BooksFragment.kt
@@ -312,7 +312,7 @@ class BooksFragment : CommonFragment(), DrawerItem, OnViewHolderClickListener<Bo
         }
 
     private fun exportBook(book: Book, format: BookFormat) {
-        val defaultFileName = BookName.fileName(book.name, format)
+        val defaultFileName = BookName.lastPathSegment(book.name, format)
         pickFileForBookExport.launch(defaultFileName)
     }
 
@@ -599,7 +599,7 @@ class BooksFragment : CommonFragment(), DrawerItem, OnViewHolderClickListener<Bo
     private fun guessBookNameFromUri(uri: Uri): String? {
         val fileName: String = BookName.getFileName(requireContext(), uri)
         return if (BookName.isSupportedFormatFileName(fileName)) {
-            val bookName = BookName.fromFileName(fileName)
+            val bookName = BookName.fromRepoRelativePath(fileName)
             bookName.name
         } else {
             null
diff --git a/app/src/main/java/com/orgzly/android/usecase/LinkFindTarget.kt b/app/src/main/java/com/orgzly/android/usecase/LinkFindTarget.kt
index c8e3220d5..21fc4e6e2 100644
--- a/app/src/main/java/com/orgzly/android/usecase/LinkFindTarget.kt
+++ b/app/src/main/java/com/orgzly/android/usecase/LinkFindTarget.kt
@@ -39,7 +39,7 @@ class LinkFindTarget(val path: String) : UseCase() {
         val file = File(path)
 
         return if (!hasParent(file) && BookName.isSupportedFormatFileName(file.name)) {
-            BookName.fromFileName(file.name)
+            BookName.fromRepoRelativePath(file.name)
         } else {
             null
         }
diff --git a/app/src/main/java/com/orgzly/android/util/MiscUtils.java b/app/src/main/java/com/orgzly/android/util/MiscUtils.java
index 8546cb7d8..0c5f5c539 100644
--- a/app/src/main/java/com/orgzly/android/util/MiscUtils.java
+++ b/app/src/main/java/com/orgzly/android/util/MiscUtils.java
@@ -66,6 +66,22 @@ public static String readStringFromFile(File file) throws IOException {
         return fileData.toString();
     }
 
+    public static String readStringFromDocumentFile(DocumentFile file) throws IOException {
+        ContentResolver contentResolver = App.getAppContext().getContentResolver();
+        StringBuilder fileData = new StringBuilder();
+        try (InputStream inputStream = contentResolver.openInputStream(file.getUri())) {
+            try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
+                char[] buf = new char[1024];
+                int numRead;
+                while ((numRead = reader.read(buf)) != -1) {
+                    String readData = String.valueOf(buf, 0, numRead);
+                    fileData.append(readData);
+                }
+            }
+        }
+        return fileData.toString();
+    }
+
     public static void writeStringToFile(String str, File file) throws FileNotFoundException {
         try (PrintWriter out = new PrintWriter(file)) {
             out.write(str);
diff --git a/app/src/main/java/com/orgzly/android/util/UriUtils.java b/app/src/main/java/com/orgzly/android/util/UriUtils.java
index 0c2eaf3db..b70e8e4b4 100644
--- a/app/src/main/java/com/orgzly/android/util/UriUtils.java
+++ b/app/src/main/java/com/orgzly/android/util/UriUtils.java
@@ -47,10 +47,10 @@ public static Uri uriFromPath(String schema, String directory) {
      * Replaces the name part of the uri, leaving everything (including the extension) the same.
      */
     public static Uri getUriForNewName(Uri uri, String name) {
-        BookName bookName = BookName.fromFileName(uri.getLastPathSegment());
+        BookName bookName = BookName.fromRepoRelativePath(uri.getLastPathSegment());
         BookFormat format = bookName.getFormat();
 
-        String newFilename = BookName.fileName(name, format);
+        String newFilename = BookName.repoRelativePath(name, format);
 
         return UriUtils.dirUri(uri) // Old Uri without file name
                 .buildUpon()