From b70f8666562470102ca91a872d940519f66c30ce Mon Sep 17 00:00:00 2001 From: hius07 <62179190+hius07@users.noreply.github.com> Date: Tue, 31 Oct 2023 07:30:39 +0200 Subject: [PATCH] DocSettings (again) (#11020) Cleaning and optimizing Docsettings code. --- frontend/apps/filemanager/filemanager.lua | 10 +- .../apps/filemanager/filemanagerbookinfo.lua | 153 ++++- frontend/apps/filemanager/filemanagermenu.lua | 71 +-- frontend/apps/filemanager/filemanagerutil.lua | 8 +- frontend/docsettings.lua | 525 ++++++++---------- frontend/document/credocument.lua | 2 +- .../elements/common_settings_menu_table.lua | 49 +- spec/unit/docsettings_spec.lua | 92 ++- spec/unit/filemanager_spec.lua | 4 +- 9 files changed, 428 insertions(+), 486 deletions(-) diff --git a/frontend/apps/filemanager/filemanager.lua b/frontend/apps/filemanager/filemanager.lua index f9c8e75a1..e5eb88f91 100644 --- a/frontend/apps/filemanager/filemanager.lua +++ b/frontend/apps/filemanager/filemanager.lua @@ -10,6 +10,7 @@ local DocSettings = require("docsettings") local DocumentRegistry = require("document/documentregistry") local Event = require("ui/event") local FileChooser = require("ui/widget/filechooser") +local FileManagerBookInfo = require("apps/filemanager/filemanagerbookinfo") local FileManagerCollection = require("apps/filemanager/filemanagercollection") local FileManagerConverter = require("apps/filemanager/filemanagerconverter") local FileManagerFileSearcher = require("apps/filemanager/filemanagerfilesearcher") @@ -415,6 +416,7 @@ function FileManager:init() self:registerModule("menu", self.menu) self:registerModule("history", FileManagerHistory:new{ ui = self }) + self:registerModule("bookinfo", FileManagerBookInfo:new{ ui = self }) self:registerModule("collections", FileManagerCollection:new{ ui = self }) self:registerModule("filesearcher", FileManagerFileSearcher:new{ ui = self }) self:registerModule("folder_shortcuts", FileManagerShortcuts:new{ ui = self }) @@ -879,7 +881,7 @@ function FileManager:pasteHere(file) local function infoCopyFile() if self:copyRecursive(orig_file, dest_path) then if is_file then - DocSettings:updateLocation(orig_file, dest_file, true) + DocSettings.updateLocation(orig_file, dest_file, true) end return true else @@ -893,7 +895,7 @@ function FileManager:pasteHere(file) local function infoMoveFile() if self:moveFile(orig_file, dest_path) then if is_file then - DocSettings:updateLocation(orig_file, dest_file) + DocSettings.updateLocation(orig_file, dest_file) ReadHistory:updateItemByPath(orig_file, dest_file) -- (will update "lastfile" if needed) else ReadHistory:updateItemsByPath(orig_file, dest_file) @@ -1018,7 +1020,7 @@ function FileManager:deleteFile(file, is_file) end if ok and not err then if is_file then - DocSettings:updateLocation(file) + DocSettings.updateLocation(file) ReadHistory:fileDeleted(file) end ReadCollection:removeItemByPath(file, not is_file) @@ -1067,7 +1069,7 @@ function FileManager:renameFile(file, basename, is_file) local function doRenameFile() if self:moveFile(file, dest) then if is_file then - DocSettings:updateLocation(file, dest) + DocSettings.updateLocation(file, dest) ReadHistory:updateItemByPath(file, dest) -- (will update "lastfile" if needed) else ReadHistory:updateItemsByPath(file, dest) diff --git a/frontend/apps/filemanager/filemanagerbookinfo.lua b/frontend/apps/filemanager/filemanagerbookinfo.lua index 932673af4..f7d8fc6c2 100644 --- a/frontend/apps/filemanager/filemanagerbookinfo.lua +++ b/frontend/apps/filemanager/filemanagerbookinfo.lua @@ -19,6 +19,8 @@ local filemanagerutil = require("apps/filemanager/filemanagerutil") local lfs = require("libs/libkoreader-lfs") local util = require("util") local _ = require("gettext") +local N_ = _.ngettext +local T = require("ffi/util").template local BookInfo = WidgetContainer:extend{ props = { @@ -44,7 +46,7 @@ local BookInfo = WidgetContainer:extend{ } function BookInfo:init() - if self.ui then -- only for Reader menu + if self.document then -- only for Reader menu self.ui.menu:registerToMainMenu(self) end end @@ -83,7 +85,7 @@ function BookInfo:show(file, book_props) book_props = BookInfo.getDocProps(file, book_props) end -- cover image - self.custom_book_cover = DocSettings:findCoverFile(file) + self.custom_book_cover = DocSettings:findCustomCoverFile(file) local key_text = self.prop_text["cover"] if self.custom_book_cover then key_text = "\u{F040} " .. key_text @@ -99,9 +101,9 @@ function BookInfo:show(file, book_props) }) -- metadata local custom_props - local custom_metadata_file = DocSettings:getCustomMetadataFile(file) + local custom_metadata_file = DocSettings:findCustomMetadataFile(file) if custom_metadata_file then - self.custom_doc_settings = DocSettings:openCustomMetadata(custom_metadata_file) + self.custom_doc_settings = DocSettings.openSettingsFile(custom_metadata_file) custom_props = self.custom_doc_settings:readSetting("custom_props") end local values_lang @@ -173,17 +175,17 @@ function BookInfo:show(file, book_props) end function BookInfo.getCustomProp(prop_key, filepath) - local custom_metadata_file = DocSettings:getCustomMetadataFile(filepath) + local custom_metadata_file = DocSettings:findCustomMetadataFile(filepath) return custom_metadata_file - and DocSettings:openCustomMetadata(custom_metadata_file):readSetting("custom_props")[prop_key] + and DocSettings.openSettingsFile(custom_metadata_file):readSetting("custom_props")[prop_key] end -- Returns extended and customized metadata. function BookInfo.extendProps(original_props, filepath) -- do not customize if filepath is not passed (eg from covermenu) - local custom_metadata_file = filepath and DocSettings:getCustomMetadataFile(filepath) + local custom_metadata_file = filepath and DocSettings:findCustomMetadataFile(filepath) local custom_props = custom_metadata_file - and DocSettings:openCustomMetadata(custom_metadata_file):readSetting("custom_props") or {} + and DocSettings.openSettingsFile(custom_metadata_file):readSetting("custom_props") or {} original_props = original_props or {} local props = {} @@ -226,9 +228,9 @@ function BookInfo.getDocProps(file, book_props, no_open_document) -- If still no book_props (book never opened or empty "stats"), -- but custom metadata exists, it has a copy of original doc_props if not book_props then - local custom_metadata_file = DocSettings:getCustomMetadataFile(file) + local custom_metadata_file = DocSettings:findCustomMetadataFile(file) if custom_metadata_file then - book_props = DocSettings:openCustomMetadata(custom_metadata_file):readSetting("doc_props") + book_props = DocSettings.openSettingsFile(custom_metadata_file):readSetting("doc_props") end end @@ -320,7 +322,7 @@ function BookInfo:getCoverImage(doc, file, force_orig) local cover_bb -- check for a custom cover (orig cover is forcibly requested in "Book information" only) if not force_orig then - local custom_cover = DocSettings:findCoverFile(file or (doc and doc.file)) + local custom_cover = DocSettings:findCustomCoverFile(file or (doc and doc.file)) if custom_cover then local cover_doc = DocumentRegistry:openDocument(custom_cover) if cover_doc then @@ -348,8 +350,8 @@ function BookInfo:getCoverImage(doc, file, force_orig) end function BookInfo:updateBookInfo(file, book_props, prop_updated, prop_value_old) - if prop_updated == "cover" and self.ui then - self.ui.doc_settings:getCoverFile(true) -- reset cover file cache + if self.document and prop_updated == "cover" then + self.ui.doc_settings:getCustomCoverFile(true) -- reset cover file cache end self.prop_updated = { filepath = file, @@ -361,10 +363,10 @@ function BookInfo:updateBookInfo(file, book_props, prop_updated, prop_value_old) self:show(file, book_props) end -function BookInfo:setCustomBookCover(file, book_props) +function BookInfo:setCustomCover(file, book_props) if self.custom_book_cover then -- reset custom cover if os.remove(self.custom_book_cover) then - DocSettings:removeSidecarDir(file, util.splitFilePathName(self.custom_book_cover)) + DocSettings.removeSidecarDir(util.splitFilePathName(self.custom_book_cover)) self:updateBookInfo(file, book_props, "cover") end else -- choose an image and set custom cover @@ -386,28 +388,27 @@ end function BookInfo:setCustomMetadata(file, book_props, prop_key, prop_value) -- in file - local custom_doc_settings, custom_props, display_title + local custom_doc_settings, custom_props, display_title, no_custom_metadata if self.custom_doc_settings then custom_doc_settings = self.custom_doc_settings - custom_props = custom_doc_settings:readSetting("custom_props") else -- no custom metadata file, create new - custom_doc_settings = DocSettings:openCustomMetadata() - custom_props = {} + custom_doc_settings = DocSettings.openSettingsFile() display_title = book_props.display_title -- backup book_props.display_title = nil custom_doc_settings:saveSetting("doc_props", book_props) -- save a copy of original props end + custom_props = custom_doc_settings:readSetting("custom_props", {}) local prop_value_old = custom_props[prop_key] or book_props[prop_key] custom_props[prop_key] = prop_value -- nil when resetting a custom prop if next(custom_props) == nil then -- no more custom metadata - os.remove(custom_doc_settings.custom_metadata_file) - DocSettings:removeSidecarDir(file, util.splitFilePathName(custom_doc_settings.custom_metadata_file)) + os.remove(custom_doc_settings.sidecar_file) + DocSettings.removeSidecarDir(util.splitFilePathName(custom_doc_settings.sidecar_file)) + no_custom_metadata = true else if book_props.pages then -- keep a copy of original 'pages' up to date local original_props = custom_doc_settings:readSetting("doc_props") original_props.pages = book_props.pages end - custom_doc_settings:saveSetting("custom_props", custom_props) custom_doc_settings:flushCustomMetadata(file) end book_props.display_title = book_props.display_title or display_title -- restore @@ -417,11 +418,13 @@ function BookInfo:setCustomMetadata(file, book_props, prop_key, prop_value) if prop_key == "title" then -- generate when resetting the customized title and original is empty book_props.display_title = book_props.title or filemanagerutil.splitFileNameType(file) end - local ui = self.ui or require("apps/reader/readerui").instance - if ui and ui.document and ui.document.file == file then -- currently opened document - ui.doc_props[prop_key] = prop_value + if self.document and self.document.file == file then -- currently opened document + self.ui.doc_props[prop_key] = prop_value if prop_key == "title" then - ui.doc_props.display_title = book_props.display_title + self.ui.doc_props.display_title = book_props.display_title + end + if no_custom_metadata then + self.ui.doc_settings:getCustomMetadataFile(true) -- reset metadata file cache end end self:updateBookInfo(file, book_props, prop_key, prop_value_old) @@ -517,7 +520,7 @@ function BookInfo:showCustomDialog(file, book_props, prop_key) ok_callback = function() UIManager:close(button_dialog) if prop_is_cover then - self:setCustomBookCover(file, book_props) + self:setCustomCover(file, book_props) else self:setCustomMetadata(file, book_props, prop_key) end @@ -532,7 +535,7 @@ function BookInfo:showCustomDialog(file, book_props, prop_key) callback = function() UIManager:close(button_dialog) if prop_is_cover then - self:setCustomBookCover(file, book_props) + self:setCustomCover(file, book_props) else self:showCustomEditDialog(file, book_props, prop_key) end @@ -548,4 +551,98 @@ function BookInfo:showCustomDialog(file, book_props, prop_key) UIManager:show(button_dialog) end +function BookInfo:moveBookMetadata() + -- called by filemanagermenu only + local file_chooser = self.ui.file_chooser + local function scanPath() + local sys_folders = { -- do not scan sys_folders + ["/dev"] = true, + ["/proc"] = true, + ["/sys"] = true, + } + local books_to_move = {} + local dirs = { file_chooser.path } + while #dirs ~= 0 do + local new_dirs = {} + for _, d in ipairs(dirs) do + local ok, iter, dir_obj = pcall(lfs.dir, d) + if ok then + for f in iter, dir_obj do + local fullpath = "/" .. f + if d ~= "/" then + fullpath = d .. fullpath + end + local attributes = lfs.attributes(fullpath) or {} + if attributes.mode == "directory" and f ~= "." and f ~= ".." + and file_chooser:show_dir(f) and not sys_folders[fullpath] then + table.insert(new_dirs, fullpath) + elseif attributes.mode == "file" and not util.stringStartsWith(f, "._") + and file_chooser:show_file(f) + and DocSettings.isSidecarFileNotInPreferredLocation(fullpath) then + table.insert(books_to_move, fullpath) + end + end + end + end + dirs = new_dirs + end + return books_to_move + end + UIManager:show(ConfirmBox:new{ + text = _("Scan books in current folder and subfolders for their metadata location?"), + ok_text = _("Scan"), + ok_callback = function() + local books_to_move = scanPath() + local books_to_move_nb = #books_to_move + if books_to_move_nb == 0 then + UIManager:show(InfoMessage:new{ + text = _("No books with metadata not in your preferred location found."), + }) + else + UIManager:show(ConfirmBox:new{ + text = T(N_("1 book with metadata not in your preferred location found.", + "%1 books with metadata not in your preferred location found.", + books_to_move_nb), books_to_move_nb) .. "\n" .. + _("Move book metadata to your preferred location?"), + ok_text = _("Move"), + ok_callback = function() + UIManager:close(self.menu_container) + for _, book in ipairs(books_to_move) do + DocSettings.updateLocation(book, book) + end + file_chooser:refreshPath() + end, + }) + end + end, + }) +end + +function BookInfo.showBooksWithHashBasedMetadata() + local header = T(_("Hash-based metadata has been saved in %1 for the following documents. Hash-based storage may slow down file browser navigation in large directories. Thus, if not using hash-based metadata storage, it is recommended to open the associated documents in KOReader to automatically migrate their metadata to the preferred storage location, or to delete %1, which will speed up file browser navigation."), + DocSettings.getSidecarStorage("hash")) + local file_info = { header .. "\n" } + local sdrs = DocSettings.findSidecarFilesInHashLocation() + for i, sdr in ipairs(sdrs) do + local sidecar_file, custom_metadata_file = unpack(sdr) + local doc_settings = DocSettings.openSettingsFile(sidecar_file) + local doc_props = doc_settings:readSetting("doc_props") + local custom_props = custom_metadata_file + and DocSettings.openSettingsFile(custom_metadata_file):readSetting("custom_props") or {} + local doc_path = doc_settings:readSetting("doc_path") + local title = custom_props.title or doc_props.title or filemanagerutil.splitFileNameType(doc_path) + local author = custom_props.authors or doc_props.authors or _("N/A") + doc_path = lfs.attributes(doc_path, "mode") == "file" and doc_path or _("N/A") + local text = T(_("%1. Title: %2; Author: %3\nDocument: %4"), i, title, author, doc_path) + table.insert(file_info, text) + end + local doc_nb = #file_info - 1 + UIManager:show(TextViewer:new{ + title = T(N_("1 document with hash-based metadata", "%1 documents with hash-based metadata", doc_nb), doc_nb), + title_multilines = true, + justified = false, + text = table.concat(file_info, "\n"), + }) +end + return BookInfo diff --git a/frontend/apps/filemanager/filemanagermenu.lua b/frontend/apps/filemanager/filemanagermenu.lua index d128039ec..5956ff0a7 100644 --- a/frontend/apps/filemanager/filemanagermenu.lua +++ b/frontend/apps/filemanager/filemanagermenu.lua @@ -15,7 +15,6 @@ local lfs = require("libs/libkoreader-lfs") local logger = require("logger") local util = require("util") local _ = require("gettext") -local N_ = _.ngettext local T = FFIUtil.template local FileManagerMenu = InputContainer:extend{ @@ -489,7 +488,7 @@ To: text = _("Move book metadata"), keep_menu_open = true, callback = function() - self:moveBookMetadata() + self.ui.bookinfo:moveBookMetadata() end, } @@ -930,74 +929,6 @@ function FileManagerMenu:getStartWithMenuTable() } end -function FileManagerMenu:moveBookMetadata() - local DocSettings = require("docsettings") - local FileChooser = self.ui.file_chooser - local function scanPath() - local sys_folders = { -- do not scan sys_folders - ["/dev"] = true, - ["/proc"] = true, - ["/sys"] = true, - } - local books_to_move = {} - local dirs = {FileChooser.path} - while #dirs ~= 0 do - local new_dirs = {} - for _, d in ipairs(dirs) do - local ok, iter, dir_obj = pcall(lfs.dir, d) - if ok then - for f in iter, dir_obj do - local fullpath = "/" .. f - if d ~= "/" then - fullpath = d .. fullpath - end - local attributes = lfs.attributes(fullpath) or {} - if attributes.mode == "directory" and f ~= "." and f ~= ".." - and FileChooser:show_dir(f) and not sys_folders[fullpath] then - table.insert(new_dirs, fullpath) - elseif attributes.mode == "file" and not util.stringStartsWith(f, "._") - and FileChooser:show_file(f) and DocSettings:hasSidecarFile(fullpath) - and lfs.attributes(DocSettings:getSidecarFile(fullpath), "mode") ~= "file" then - table.insert(books_to_move, fullpath) - end - end - end - end - dirs = new_dirs - end - return books_to_move - end - UIManager:show(ConfirmBox:new{ - text = _("Scan books in current folder and subfolders for their metadata location?"), - ok_text = _("Scan"), - ok_callback = function() - local books_to_move = scanPath() - local books_to_move_nb = #books_to_move - if books_to_move_nb == 0 then - local InfoMessage = require("ui/widget/infomessage") - UIManager:show(InfoMessage:new{ - text = _("No books with metadata not in your preferred location found."), - }) - else - UIManager:show(ConfirmBox:new{ - text = T(N_("1 book with metadata not in your preferred location found.", - "%1 books with metadata not in your preferred location found.", - books_to_move_nb), books_to_move_nb) .. "\n" .. - _("Move book metadata to your preferred location?"), - ok_text = _("Move"), - ok_callback = function() - UIManager:close(self.menu_container) - for _, book in ipairs(books_to_move) do - DocSettings:updateLocation(book, book) - end - FileChooser:refreshPath() - end, - }) - end - end, - }) -end - function FileManagerMenu:exitOrRestart(callback, force) UIManager:close(self.menu_container) diff --git a/frontend/apps/filemanager/filemanagerutil.lua b/frontend/apps/filemanager/filemanagerutil.lua index 50d70292b..9fc209a2d 100644 --- a/frontend/apps/filemanager/filemanagerutil.lua +++ b/frontend/apps/filemanager/filemanagerutil.lua @@ -142,9 +142,9 @@ end function filemanagerutil.genResetSettingsButton(file, caller_callback, button_disabled) file = ffiutil.realpath(file) or file local has_sidecar_file = DocSettings:hasSidecarFile(file) - local custom_cover_file = DocSettings:findCoverFile(file) + local custom_cover_file = DocSettings:findCustomCoverFile(file) local has_custom_cover_file = custom_cover_file and true or false - local custom_metadata_file = DocSettings:getCustomMetadataFile(file) + local custom_metadata_file = DocSettings:findCustomMetadataFile(file) local has_custom_metadata_file = custom_metadata_file and true or false return { text = _("Reset"), @@ -162,8 +162,8 @@ function filemanagerutil.genResetSettingsButton(file, caller_callback, button_di ok_callback = function() local data_to_purge = { doc_settings = check_button_settings.checked, - custom_cover_file = check_button_cover.checked, - custom_metadata_file = check_button_metadata.checked, + custom_cover_file = check_button_cover.checked and custom_cover_file, + custom_metadata_file = check_button_metadata.checked and custom_metadata_file, } DocSettings:open(file):purge(nil, data_to_purge) if data_to_purge.custom_cover_file or data_to_purge.custom_metadata_file then diff --git a/frontend/docsettings.lua b/frontend/docsettings.lua index c10360513..0308755eb 100644 --- a/frontend/docsettings.lua +++ b/frontend/docsettings.lua @@ -19,12 +19,28 @@ local DOCSETTINGS_DIR = DataStorage:getDocSettingsDir() local DOCSETTINGS_HASH_DIR = DataStorage:getDocSettingsHashDir() local custom_metadata_filename = "custom_metadata.lua" -local is_hash_location_enabled +function DocSettings.getSidecarStorage(location) + if location == "dir" then + return DOCSETTINGS_DIR + elseif location == "hash" then + return DOCSETTINGS_HASH_DIR + end +end + +local function isDir(dir) + return lfs.attributes(dir, "mode") == "directory" +end + +local function isFile(file) + return lfs.attributes(file, "mode") == "file" +end + local doc_hash_cache = {} +local is_hash_location_enabled function DocSettings.isHashLocationEnabled() if is_hash_location_enabled == nil then - is_hash_location_enabled = lfs.attributes(DOCSETTINGS_HASH_DIR, "mode") == "directory" + is_hash_location_enabled = isDir(DOCSETTINGS_HASH_DIR) end return is_hash_location_enabled end @@ -33,14 +49,13 @@ function DocSettings.setIsHashLocationEnabled(value) is_hash_location_enabled = value end - local function buildCandidates(list) local candidates = {} local previous_entry_exists = false for i, file_path in ipairs(list) do -- Ignore missing files. - if file_path ~= "" and lfs.attributes(file_path, "mode") == "file" then + if file_path ~= "" and isFile(file_path) then local mtime = lfs.attributes(file_path, "modification") -- NOTE: Extra trickery: if we're inserting a "backup" file, and its primary buddy exists, -- make sure it will *never* sort ahead of it by using the same mtime. @@ -81,6 +96,18 @@ local function buildCandidates(list) return candidates end +local function getOrderedLocationCandidates() + local preferred_location = G_reader_settings:readSetting("document_metadata_folder", "doc") + if preferred_location == "hash" then + return { "hash", "doc", "dir" } + end + local candidates = preferred_location == "doc" and { "doc", "dir" } or { "dir", "doc" } + if DocSettings.isHashLocationEnabled() then + table.insert(candidates, "hash") + end + return candidates +end + --- Returns path to sidecar directory (`filename.sdr`). -- Sidecar directory is the file without _last_ suffix. -- @string doc_path path to the document (e.g., `/foo/bar.pdf`) @@ -111,52 +138,44 @@ function DocSettings:getSidecarDir(doc_path, force_location) return path .. ".sdr" end ---- Returns path to `metadata.lua` file. --- @string doc_path path to the document (e.g., `/foo/bar.pdf`) --- @treturn string path to `/foo/bar.sdr/metadata.lua` file -function DocSettings:getSidecarFile(doc_path, force_location) - if doc_path == nil or doc_path == "" then return "" end - -- If the file does not have a suffix or we are working on a directory, we - -- should ignore the suffix part in metadata file path. - local suffix = doc_path:match(".*%.(.+)") or "" - return self:getSidecarDir(doc_path, force_location) .. "/metadata." .. suffix .. ".lua" +function DocSettings.getSidecarFilename(doc_path) + local suffix = doc_path:match(".*%.(.+)") or "_" + return "metadata." .. suffix .. ".lua" end --- Returns `true` if there is a `metadata.lua` file. -- @string doc_path path to the document (e.g., `/foo/bar.pdf`) -- @treturn bool function DocSettings:hasSidecarFile(doc_path) - return self:getDocSidecarFile(doc_path) and true or false + return self:findSidecarFile(doc_path) and true or false end --- Returns path of `metadata.lua` file if it exists, or nil. -- @string doc_path path to the document (e.g., `/foo/bar.pdf`) -- @bool no_legacy set to true to skip check of the legacy history file -- @treturn string -function DocSettings:getDocSidecarFile(doc_path, no_legacy) - local sidecar_file = self:getSidecarFile(doc_path, "doc") - if lfs.attributes(sidecar_file, "mode") == "file" then - return sidecar_file - end - sidecar_file = self:getSidecarFile(doc_path, "dir") - if lfs.attributes(sidecar_file, "mode") == "file" then - return sidecar_file - end - -- Calculate partial hash and check for hash-based files only if there are files to check - if DocSettings.isHashLocationEnabled() then - sidecar_file = self:getSidecarFile(doc_path, "hash") - if lfs.attributes(sidecar_file, "mode") == "file" then - return sidecar_file +function DocSettings:findSidecarFile(doc_path, no_legacy) + local sidecar_filename = DocSettings.getSidecarFilename(doc_path) + local sidecar_file + for _, location in ipairs(getOrderedLocationCandidates()) do + sidecar_file = self:getSidecarDir(doc_path, location) .. "/" .. sidecar_filename + if isFile(sidecar_file) then + return sidecar_file, location end end if not no_legacy then sidecar_file = self:getHistoryPath(doc_path) - if lfs.attributes(sidecar_file, "mode") == "file" then - return sidecar_file + if isFile(sidecar_file) then + return sidecar_file, "hist" -- for isSidecarFileNotInPreferredLocation() used in moveBookMetadata end end end +function DocSettings.isSidecarFileNotInPreferredLocation(doc_path) + local _, location = DocSettings:findSidecarFile(doc_path) + return location and location ~= G_reader_settings:readSetting("document_metadata_folder", "doc") +end + function DocSettings:getHistoryPath(doc_path) if doc_path == nil or doc_path == "" then return "" end return HISTORY_DIR .. "/[" .. doc_path:gsub("(.*/)([^/]+)", "%1] %2"):gsub("/", "#") .. ".lua" @@ -193,20 +212,6 @@ function DocSettings:getFileFromHistory(hist_name) end end ---- Returns the directory and full filepath of a hash-ID-based sidecar metadata store --- @string doc_path path to the document (e.g., `/foo/bar.pdf`) -function DocSettings:getSidecarHashDirAndFilepath(doc_path) - -- Getting PDF ID from trailer via mupdf has not been implemented - everything uses partial MD5 - local path = self:getSidecarDir(doc_path, "hash") - local filetype = doc_path:match(".+%.(%w+)$") - if not filetype or filetype == "" then - return "", "" - end - local hash_file = "metadata." .. filetype .. ".lua" - local hash_filepath = path .. "/" .. hash_file - return path, hash_filepath -end - --- Opens a document's individual settings (font, margin, dictionary, etc.) -- @string doc_path path to the document (e.g., `/foo/bar.pdf`) -- @treturn DocSettings object @@ -214,28 +219,25 @@ function DocSettings:open(doc_path) -- NOTE: Beware, our new instance is new, but self is still DocSettings! local new = DocSettings:extend{} + new.sidecar_filename = DocSettings.getSidecarFilename(doc_path) + new.doc_sidecar_dir = new:getSidecarDir(doc_path, "doc") - new.doc_sidecar_file = new:getSidecarFile(doc_path, "doc") local doc_sidecar_file, legacy_sidecar_file - if lfs.attributes(new.doc_sidecar_dir, "mode") == "directory" then - doc_sidecar_file = new.doc_sidecar_file + if isDir(new.doc_sidecar_dir) then + doc_sidecar_file = new.doc_sidecar_dir .. "/" .. new.sidecar_filename legacy_sidecar_file = new.doc_sidecar_dir .. "/" .. ffiutil.basename(doc_path) .. ".lua" end new.dir_sidecar_dir = new:getSidecarDir(doc_path, "dir") - new.dir_sidecar_file = new:getSidecarFile(doc_path, "dir") local dir_sidecar_file - if lfs.attributes(new.dir_sidecar_dir, "mode") == "directory" then - dir_sidecar_file = new.dir_sidecar_file + if isDir(new.dir_sidecar_dir) then + dir_sidecar_file = new.dir_sidecar_dir .. "/" .. new.sidecar_filename end - local history_file = new:getHistoryPath(doc_path) - - local hash_sidecar_dir, hash_sidecar_file + local hash_sidecar_file if DocSettings.isHashLocationEnabled() then - hash_sidecar_dir, hash_sidecar_file = - new:getSidecarHashDirAndFilepath(doc_path) - new.hash_sidecar_dir = hash_sidecar_dir - new.hash_sidecar_file = hash_sidecar_file + new.hash_sidecar_dir = new:getSidecarDir(doc_path, "hash") + hash_sidecar_file = new.hash_sidecar_dir .. "/" .. new.sidecar_filename end + local history_file = new:getHistoryPath(doc_path) -- Candidates list, in order of priority: local candidates_list = { @@ -249,10 +251,10 @@ function DocSettings:open(doc_path) dir_sidecar_file or "", -- Backup file of new sidecar file in docsettings folder dir_sidecar_file and (dir_sidecar_file .. ".old") or "", - -- Hash or PDF fingerprint-based sidecar file lookup + -- New sidecar file in hashdocsettings folder hash_sidecar_file or "", - -- Backup file of hash or PDF fingerprint-based sidecar file lookup - hash_sidecar_file and (new.hash_sidecar_file .. ".old") or "", + -- Backup file of new sidecar file in hashdocsettings folder + hash_sidecar_file and (hash_sidecar_file .. ".old") or "", -- Legacy history folder history_file, -- Backup file in legacy history folder @@ -290,52 +292,69 @@ function DocSettings:open(doc_path) return new end +--- Light version of open(). Opens a sidecar file or a custom metadata file. +-- Returned object cannot be used to save changes to the sidecar file (flush()). +-- Must be used to save changes to the custom metadata file (flushCustomMetadata()). +function DocSettings.openSettingsFile(sidecar_file) + local new = DocSettings:extend{} + local ok, stored + if sidecar_file then + ok, stored = pcall(dofile, sidecar_file) + end + if ok and next(stored) ~= nil then + new.data = stored + else + new.data = {} + end + new.sidecar_file = sidecar_file + return new +end + --- Serializes settings and writes them to `metadata.lua`. function DocSettings:flush(data, no_custom_metadata) - -- Depending on the settings, doc_settings are saved to the book folder or - -- to koreader/docsettings folder. The latter is also a fallback for read-only book storage. - local serials - local preferred_metdata_storage = G_reader_settings:readSetting("document_metadata_folder", "doc") - if preferred_metdata_storage == "doc" then - serials = { {self.doc_sidecar_dir, self.doc_sidecar_file}, - {self.dir_sidecar_dir, self.dir_sidecar_file}, } - elseif preferred_metdata_storage == "dir" then - serials = { {self.dir_sidecar_dir, self.dir_sidecar_file}, } - elseif preferred_metdata_storage == "hash" then - if self.hash_sidecar_dir == nil or self.hash_sidecar_file == nil then - self.hash_sidecar_dir, self.hash_sidecar_file = - self:getSidecarHashDirAndFilepath(self.data.doc_path) + data = data or self.data + local sidecar_dirs + local preferred_location = G_reader_settings:readSetting("document_metadata_folder", "doc") + if preferred_location == "doc" then + sidecar_dirs = { self.doc_sidecar_dir, self.dir_sidecar_dir } -- fallback for read-only book storage + elseif preferred_location == "dir" then + sidecar_dirs = { self.dir_sidecar_dir } + elseif preferred_location == "hash" then + if self.hash_sidecar_dir == nil then + self.hash_sidecar_dir = self:getSidecarDir(data.doc_path, "hash") end - serials = { {self.hash_sidecar_dir, self.hash_sidecar_file } } + sidecar_dirs = { self.hash_sidecar_dir } end - local s_out = dump(data or self.data, nil, true) - for _, s in ipairs(serials) do - local sidecar_dir, sidecar_file = unpack(s) + local ser_data = dump(data, nil, true) + for _, sidecar_dir in ipairs(sidecar_dirs) do + local sidecar_dir_slash = sidecar_dir .. "/" + local sidecar_file = sidecar_dir_slash .. self.sidecar_filename util.makePath(sidecar_dir) logger.dbg("DocSettings: Writing to", sidecar_file) - local directory_updated = LuaSettings:backup(sidecar_file) - if util.writeToFile(s_out, sidecar_file, true, true, directory_updated) then + local directory_updated = LuaSettings:backup(sidecar_file) -- "*.old" + if util.writeToFile(ser_data, sidecar_file, true, true, directory_updated) then -- move custom cover file and custom metadata file to the metadata file location if not no_custom_metadata then local metadata_file, filepath, filename -- custom cover - metadata_file = self:getCoverFile() + metadata_file = self:getCustomCoverFile() if metadata_file then filepath, filename = util.splitFilePathName(metadata_file) - if filepath ~= sidecar_dir .. "/" then - ffiutil.copyFile(metadata_file, sidecar_dir .. "/" .. filename) + if filepath ~= sidecar_dir_slash then + ffiutil.copyFile(metadata_file, sidecar_dir_slash .. filename) os.remove(metadata_file) - self:getCoverFile(true) -- reset cache + self:getCustomCoverFile(true) -- reset cache end end -- custom metadata metadata_file = self:getCustomMetadataFile() if metadata_file then filepath, filename = util.splitFilePathName(metadata_file) - if filepath ~= sidecar_dir .. "/" then - ffiutil.copyFile(metadata_file, sidecar_dir .. "/" .. filename) + if filepath ~= sidecar_dir_slash then + ffiutil.copyFile(metadata_file, sidecar_dir_slash .. filename) os.remove(metadata_file) + self:getCustomMetadataFile(true) -- reset cache end end end @@ -351,10 +370,10 @@ end function DocSettings:purge(sidecar_to_keep, data_to_purge) local custom_cover_file, custom_metadata_file if sidecar_to_keep == nil then - custom_cover_file = self:getCoverFile() + custom_cover_file = self:getCustomCoverFile() custom_metadata_file = self:getCustomMetadataFile() end - if data_to_purge == nil then + if data_to_purge == nil then -- purge all data_to_purge = { doc_settings = true, custom_cover_file = custom_cover_file, @@ -366,7 +385,7 @@ function DocSettings:purge(sidecar_to_keep, data_to_purge) if data_to_purge.doc_settings and self.candidates then for _, t in ipairs(self.candidates) do local candidate_path = t.path - if lfs.attributes(candidate_path, "mode") == "file" then + if isFile(candidate_path) then if (not sidecar_to_keep) or (candidate_path ~= sidecar_to_keep and candidate_path ~= sidecar_to_keep .. ".old") then os.remove(candidate_path) @@ -376,105 +395,111 @@ function DocSettings:purge(sidecar_to_keep, data_to_purge) end end + -- Remove custom if data_to_purge.custom_cover_file then os.remove(data_to_purge.custom_cover_file) - self:getCoverFile(true) -- reset cache + self:getCustomCoverFile(true) -- reset cache end if data_to_purge.custom_metadata_file then os.remove(data_to_purge.custom_metadata_file) + self:getCustomMetadataFile(true) -- reset cache end + -- Remove empty sidecar dirs if data_to_purge.doc_settings or data_to_purge.custom_cover_file or data_to_purge.custom_metadata_file then - -- remove sidecar dirs iff empty - if lfs.attributes(self.doc_sidecar_dir, "mode") == "directory" then - os.remove(self.doc_sidecar_dir) -- keep parent folders - end - if lfs.attributes(self.dir_sidecar_dir, "mode") == "directory" then - util.removePath(self.dir_sidecar_dir) -- remove empty parent folders - end - if self.hash_sidecar_dir and lfs.attributes(self.hash_sidecar_dir, "mode") == "directory" then - util.removePath(self.hash_sidecar_dir) -- remove empty parent folders + for _, dir in ipairs({ self.doc_sidecar_dir, self.dir_sidecar_dir, self.hash_sidecar_dir }) do + DocSettings.removeSidecarDir(dir) end end + DocSettings.setIsHashLocationEnabled(nil) -- reset this in case last hash book is purged end ---- Removes empty sidecar dir. -function DocSettings:removeSidecarDir(doc_path, sidecar_dir) - if sidecar_dir == self:getSidecarDir(doc_path, "doc") then - os.remove(sidecar_dir) - else - util.removePath(sidecar_dir) +--- Removes sidecar dir iff empty. +function DocSettings.removeSidecarDir(dir) + if dir and isDir(dir) then + if dir:match("^"..DOCSETTINGS_DIR) or dir:match("^"..DOCSETTINGS_HASH_DIR) then + util.removePath(dir) -- remove empty parent folders + else + os.remove(dir) -- keep parent folders + end end end --- Updates sdr location for file rename/copy/move/delete operations. -function DocSettings:updateLocation(doc_path, new_doc_path, copy) - local doc_settings, new_sidecar_dir, cover_file - if G_reader_settings:readSetting("document_metadata_folder") == "hash" then - -- none of these operations (except delete) changes the hash -> no location change - if not new_doc_path then - doc_settings = DocSettings:open(doc_path) - local cache_file_path = doc_settings:readSetting("cache_file_path") - if cache_file_path then os.remove(cache_file_path) end - cover_file = doc_settings:getCoverFile() - doc_settings:purge() - end - else - -- update metadata - if DocSettings:hasSidecarFile(doc_path) then - doc_settings = DocSettings:open(doc_path) - if new_doc_path then +function DocSettings.updateLocation(doc_path, new_doc_path, copy) + local has_sidecar_file = DocSettings:hasSidecarFile(doc_path) + local custom_cover_file = DocSettings:findCustomCoverFile(doc_path) + local custom_metadata_file = DocSettings:findCustomMetadataFile(doc_path) + if not (has_sidecar_file or custom_cover_file or custom_metadata_file) then return end + + local doc_settings = DocSettings:open(doc_path) + local do_purge + + if new_doc_path then -- copy/rename/move + if G_reader_settings:readSetting("document_metadata_folder") ~= "hash" then -- keep hash location unchanged + local new_sidecar_dir + if has_sidecar_file then local new_doc_settings = DocSettings:open(new_doc_path) - -- save doc settings to the new location, no custom metadata yet - new_sidecar_dir = new_doc_settings:flush(doc_settings.data, true) - else - local cache_file_path = doc_settings:readSetting("cache_file_path") - if cache_file_path then - os.remove(cache_file_path) - end + doc_settings.data.doc_path = new_doc_path + new_sidecar_dir = new_doc_settings:flush(doc_settings.data, true) -- without custom end - end - - -- update custom metadata - if not doc_settings then - doc_settings = DocSettings:open(doc_path) - end - cover_file = doc_settings:getCoverFile() - if new_doc_path then - -- custom cover - if cover_file then - if not new_sidecar_dir then - new_sidecar_dir = DocSettings:getSidecarDir(new_doc_path) - util.makePath(new_sidecar_dir) - end - local _, filename = util.splitFilePathName(cover_file) - ffiutil.copyFile(cover_file, new_sidecar_dir .. "/" .. filename) + if not new_sidecar_dir then + new_sidecar_dir = DocSettings:getSidecarDir(new_doc_path) + util.makePath(new_sidecar_dir) end - -- custom metadata - local metadata_file = self:getCustomMetadataFile(doc_path) - if metadata_file then - if not new_sidecar_dir then - new_sidecar_dir = DocSettings:getSidecarDir(new_doc_path) - util.makePath(new_sidecar_dir) - end - ffiutil.copyFile(metadata_file, new_sidecar_dir .. "/" .. custom_metadata_filename) + if custom_cover_file then + local _, filename = util.splitFilePathName(custom_cover_file) + ffiutil.copyFile(custom_cover_file, new_sidecar_dir .. "/" .. filename) end + if custom_metadata_file then + ffiutil.copyFile(custom_metadata_file, new_sidecar_dir .. "/" .. custom_metadata_filename) + end + do_purge = not copy end - - if not copy then - doc_settings:purge() + else -- delete + if has_sidecar_file then + local cache_file_path = doc_settings:readSetting("cache_file_path") + if cache_file_path then + os.remove(cache_file_path) + end end + do_purge = true end - if cover_file then -- after purge because purge uses cover file cache - doc_settings:getCoverFile(true) -- reset cache + if do_purge then + doc_settings.custom_cover_file = custom_cover_file -- cache + doc_settings.custom_metadata_file = custom_metadata_file -- cache + doc_settings:purge() + end +end + +-- custom section + +function DocSettings:getCustomLocationCandidates(doc_path) + local sidecar_dir + local sidecar_file = self:findSidecarFile(doc_path, true) -- new locations only + if sidecar_file then -- book was opened, write custom metadata to its sidecar dir + sidecar_dir = util.splitFilePathName(sidecar_file):sub(1, -2) + return { sidecar_dir } + end + -- new book, create sidecar dir in accordance with sdr location setting + local preferred_location = G_reader_settings:readSetting("document_metadata_folder", "doc") + if preferred_location ~= "hash" then + sidecar_dir = self:getSidecarDir(doc_path, "dir") + if preferred_location == "doc" then + local doc_sidecar_dir = self:getSidecarDir(doc_path, "doc") + return { doc_sidecar_dir, sidecar_dir } -- fallback for read-only book storage + end + else -- "hash" + sidecar_dir = self:getSidecarDir(doc_path, "hash") end + return { sidecar_dir } end -- custom cover -local function findCoverFileInDir(dir) +local function findCustomCoverFileInDir(dir) local ok, iter, dir_obj = pcall(lfs.dir, dir) if ok then for f in iter, dir_obj do @@ -486,57 +511,30 @@ local function findCoverFileInDir(dir) end --- Returns path to book custom cover file if it exists, or nil. -function DocSettings:findCoverFile(doc_path) +function DocSettings:findCustomCoverFile(doc_path) doc_path = doc_path or self.data.doc_path - local location = G_reader_settings:readSetting("document_metadata_folder", "doc") - local sidecar_dir = self:getSidecarDir(doc_path, location) - local cover_file = findCoverFileInDir(sidecar_dir) - if cover_file then return cover_file end - local candidates = {"doc", "dir"} - if DocSettings.isHashLocationEnabled() then - table.insert(candidates, "hash") - end - for _, mode in ipairs(candidates) do - if mode ~= location then - sidecar_dir = self:getSidecarDir(doc_path, mode) - cover_file = findCoverFileInDir(sidecar_dir) - if cover_file then return cover_file end + for _, location in ipairs(getOrderedLocationCandidates()) do + local sidecar_dir = self:getSidecarDir(doc_path, location) + local custom_cover_file = findCustomCoverFileInDir(sidecar_dir) + if custom_cover_file then + return custom_cover_file end end end -function DocSettings:getCoverFile(reset_cache) +function DocSettings:getCustomCoverFile(reset_cache) if reset_cache then - self.cover_file = nil + self.custom_cover_file = nil else - if self.cover_file == nil then -- fill empty cache - self.cover_file = self:findCoverFile() or false + if self.custom_cover_file == nil then -- fill empty cache + self.custom_cover_file = self:findCustomCoverFile() or false end - return self.cover_file - end -end - -function DocSettings:getCustomCandidateSidecarDirs(doc_path) - local sidecar_file = self:getDocSidecarFile(doc_path, true) -- new locations only - if sidecar_file then -- book was opened, write custom metadata to its sidecar dir - local sidecar_dir = util.splitFilePathName(sidecar_file):sub(1, -2) - return { sidecar_dir } + return self.custom_cover_file end - -- new book, create sidecar dir in accordance with sdr location setting - local dir_sidecar_dir = self:getSidecarDir(doc_path, "dir") - local preferred_metadata_storage = G_reader_settings:readSetting("document_metadata_folder", "doc") - if preferred_metadata_storage == "doc" then - local doc_sidecar_dir = self:getSidecarDir(doc_path, "doc") - return { doc_sidecar_dir, dir_sidecar_dir } -- fallback in case of readonly book storage - elseif preferred_metadata_storage == "hash" then - local hash_sidecar_dir = self:getSidecarDir(doc_path, "hash") - return { hash_sidecar_dir } - end - return { dir_sidecar_dir } end function DocSettings:flushCustomCover(doc_path, image_file) - local sidecar_dirs = self:getCustomCandidateSidecarDirs(doc_path) + local sidecar_dirs = self:getCustomLocationCandidates(doc_path) local new_cover_filename = "/cover." .. util.getFileNameSuffix(image_file):lower() for _, sidecar_dir in ipairs(sidecar_dirs) do util.makePath(sidecar_dir) @@ -550,130 +548,57 @@ end -- custom metadata --- Returns path to book custom metadata file if it exists, or nil. -function DocSettings:getCustomMetadataFile(doc_path) +function DocSettings:findCustomMetadataFile(doc_path) doc_path = doc_path or self.data.doc_path - - local candidates = {"doc", "dir"} - if DocSettings.isHashLocationEnabled() then - table.insert(candidates, "hash") - end - for _, mode in ipairs(candidates) do - local file = self:getSidecarDir(doc_path, mode) .. "/" .. custom_metadata_filename - if lfs.attributes(file, "mode") == "file" then - return file + for _, location in ipairs(getOrderedLocationCandidates()) do + local sidecar_dir = self:getSidecarDir(doc_path, location) + local custom_metadata_file = sidecar_dir .. "/" .. custom_metadata_filename + if isFile(custom_metadata_file) then + return custom_metadata_file end end end -function DocSettings:openCustomMetadata(custom_metadata_file) - local new = DocSettings:extend{} - local ok, stored - if custom_metadata_file then - ok, stored = pcall(dofile, custom_metadata_file) - end - if ok and next(stored) ~= nil then - new.data = stored +function DocSettings:getCustomMetadataFile(reset_cache) + if reset_cache then + self.custom_metadata_file = nil else - new.data = {} + if self.custom_metadata_file == nil then -- fill empty cache + self.custom_metadata_file = self:findCustomMetadataFile() or false + end + return self.custom_metadata_file end - new.custom_metadata_file = custom_metadata_file - return new end function DocSettings:flushCustomMetadata(doc_path) - local sidecar_dirs = self:getCustomCandidateSidecarDirs(doc_path) - local new_sidecar_dir + local sidecar_dirs = self:getCustomLocationCandidates(doc_path) local s_out = dump(self.data, nil, true) for _, sidecar_dir in ipairs(sidecar_dirs) do util.makePath(sidecar_dir) - if util.writeToFile(s_out, sidecar_dir .. "/" .. custom_metadata_filename, true, true) then - new_sidecar_dir = sidecar_dir .. "/" - break - end - end - -- remove old custom metadata file if it was in alternative location - if self.custom_metadata_file then - local old_sidecar_dir = util.splitFilePathName(self.custom_metadata_file) - if old_sidecar_dir ~= new_sidecar_dir then - os.remove(self.custom_metadata_file) - self:removeSidecarDir(doc_path, old_sidecar_dir) - end - end -end - --- hash-based SDR storage -local function getSdrsInDir(path) - -- Get all the metadata.filetype.lua files under directory path. - -- Derived from readerdictionary.getIfosInDir() - local sdrs = {} - local ok, iter, dir_obj = pcall(lfs.dir, path) - if ok then - for name in iter, dir_obj do - if name ~= "." and name ~= ".." then - local fullpath = path .. "/" .. name - local attributes = lfs.attributes(fullpath) - if attributes ~= nil then - if attributes.mode == "directory" then - local dirifos = getSdrsInDir(fullpath) -- recurse - for _, ifo in pairs(dirifos) do - table.insert(sdrs, ifo) - end - elseif name:match("metadata%..+%.lua$") then - table.insert(sdrs, fullpath) - end - end - end + local new_metadata_file = sidecar_dir .. "/" .. custom_metadata_filename + if util.writeToFile(s_out, new_metadata_file, true, true) then + return true end end - return sdrs end -function DocSettings.getHashDirSdrInfos() - local sdrs = getSdrsInDir(DOCSETTINGS_HASH_DIR) - local title_author_strs = {} - for _, sdr in ipairs(sdrs) do - -- Ignore empty files - if lfs.attributes(sdr, "size") > 0 then - local ok, stored - ok, stored = pcall(dofile, sdr) - -- Ignore empty tables - if ok and next(stored) ~= nil then - local info_str, custom_authors - local sdr_path = sdr:sub(1, sdr:match(".*/()") - 1) -- SDR path - local custom_metadata_file = sdr_path .. custom_metadata_filename - if custom_metadata_file then - local custom = DocSettings:openCustomMetadata(custom_metadata_file) - local custom_props = custom:readSetting("custom_props") - if custom_props then - if custom_props.title then info_str = custom_props.title end - if custom_props.authors then custom_authors = custom_props.authors end - end - end - if not info_str then info_str = stored.doc_props.title end - if not info_str then info_str = "untitled document" end - if custom_authors then - info_str = info_str .. ", author: " .. custom_authors - elseif stored.doc_props.authors then - info_str = info_str .. ", author: " .. stored.doc_props.authors - end - if stored.stats then - if stored.stats.highlights > 0 then - info_str = info_str .. ", highlights: " .. stored.stats.highlights - end - if stored.stats.notes > 0 then - info_str = info_str .. ", notes: " .. stored.stats.notes - end - end - info_str = info_str .. ", path: " .. sdr:sub(sdr:find("/", 3) + 1) - table.insert(title_author_strs, info_str) - else - table.insert(title_author_strs, "error " .. sdr) +-- "hash" section + +-- Returns the list of pairs {sidecar_file, custom_metadata_file}. +function DocSettings.findSidecarFilesInHashLocation() + local res = {} + local callback = function(fullpath, name) + if name:match("metadata%..+%.lua$") then + local sdr = { fullpath } + local custom_metadata_file = fullpath:gsub(name, custom_metadata_filename) + if isFile(custom_metadata_file) then + table.insert(sdr, custom_metadata_file) end - else - table.insert(title_author_strs, "zero-size file " .. sdr) + table.insert(res, sdr) end end - return title_author_strs + util.findFiles(DOCSETTINGS_HASH_DIR, callback) + return res end return DocSettings diff --git a/frontend/document/credocument.lua b/frontend/document/credocument.lua index 5e0f25b67..d7d976394 100644 --- a/frontend/document/credocument.lua +++ b/frontend/document/credocument.lua @@ -149,7 +149,7 @@ function CreDocument:init() self.flows = {} self.page_in_flow = {} - local file_type = string.lower(string.match(self.file, ".+%.([^.]+)")) + local file_type = string.lower(string.match(self.file, ".+%.([^.]+)") or "") if file_type == "zip" then -- NuPogodi, 20.05.12: read the content of zip-file -- and return extention of the 1st file diff --git a/frontend/ui/elements/common_settings_menu_table.lua b/frontend/ui/elements/common_settings_menu_table.lua index 2071c4fe0..e4fd074c1 100644 --- a/frontend/ui/elements/common_settings_menu_table.lua +++ b/frontend/ui/elements/common_settings_menu_table.lua @@ -1,13 +1,13 @@ -local DataStorage = require("datastorage") local DateTimeWidget = require("ui/widget/datetimewidget") local Device = require("device") +local DocSettings = require("docsettings") local Event = require("ui/event") +local FileManagerBookInfo = require("apps/filemanager/filemanagerbookinfo") local InfoMessage = require("ui/widget/infomessage") local Language = require("ui/language") local NetworkMgr = require("ui/network/manager") local PowerD = Device:getPowerDevice() local UIManager = require("ui/uimanager") -local DocSettings = require("docsettings") local _ = require("gettext") local N_ = _.ngettext local C_ = _.pgettext @@ -541,23 +541,23 @@ common_settings.document = { } local metadata_folder_str = { - ["doc"] = _("book folder"), - ["dir"] = DataStorage:getDocSettingsDir(), - ["hash"] = DataStorage:getDocSettingsHashDir() + ["doc"] = _("book folder"), + ["dir"] = DocSettings.getSidecarStorage("dir"), + ["hash"] = DocSettings.getSidecarStorage("hash"), } -local metadata_folder_help_header = T(_([[Book view settings, reading progress, highlights, bookmarks and notes (collectively known as metadata) are stored in a separate folder named .sdr (".sdr" meaning "sidecar"). - -You can decide between three locations/methods where these will be saved:]])) -local metadata_folder_help_doc = T(_(" - alongside the book file itself (the long time default): sdr folders will be visible when you browse your library directories with another file browser or from your computer, which may clutter your vision of your library. But this allows you to move them along when you reorganize your library, and also survives any renaming of parent directories. Also, if you perform directory synchronization or backups, your settings will be part of them.")) -local metadata_folder_help_dir = T(_(" - all in %1: sdr folders will only be visible and used by KOReader, and won't clutter your vision of your library directories with another file browser or from your computer. But any reorganisation of your library (directories or filename moves and renamings) may result in KOReader not finding your previous settings for these books. These settings won't be part of any synchronization or backups of your library."), DataStorage:getDocSettingsDir()) -local metadata_folder_help_hash = T(_(" - all inside %1 as hashes: sdr folders are identified not by filepath/filename but by partial MD5 hash, allowing you to rename, move, and copy documents outside of KOReader without sdr folder clutter while keeping them linked to their metadata. However, any file modifications such as writing highlights into PDFs or downloading from calibre may change the hash, and thus lose their linked metadata. Calculating file hashes may also slow down file browser navigation. This option may suit users with multiple copies of documents across different devices and directories."), DataStorage:getDocSettingsHashDir()) -local metadata_folder_help_text = metadata_folder_help_header .. "\n" .. metadata_folder_help_doc .. "\n" .. metadata_folder_help_dir .. "\n" .. metadata_folder_help_hash +local metadata_folder_help_table = { + _("Book view settings, reading progress, highlights, bookmarks and notes (collectively known as metadata) are stored in a separate folder named .sdr (\".sdr\" meaning \"sidecar\")."), + "", + _("You can decide between three locations/methods where these will be saved:"), + _(" - alongside the book file itself (the long time default): sdr folders will be visible when you browse your library directories with another file browser or from your computer, which may clutter your vision of your library. But this allows you to move them along when you reorganize your library, and also survives any renaming of parent directories. Also, if you perform directory synchronization or backups, your settings will be part of them."), + T(_(" - all in %1: sdr folders will only be visible and used by KOReader, and won't clutter your vision of your library directories with another file browser or from your computer. But any reorganisation of your library (directories or filename moves and renamings) may result in KOReader not finding your previous settings for these books. These settings won't be part of any synchronization or backups of your library."), metadata_folder_str.dir), + T(_(" - all inside %1 as hashes: sdr folders are identified not by filepath/filename but by partial MD5 hash, allowing you to rename, move, and copy documents outside of KOReader without sdr folder clutter while keeping them linked to their metadata. However, any file modifications such as writing highlights into PDFs or downloading from calibre may change the hash, and thus lose their linked metadata. Calculating file hashes may also slow down file browser navigation. This option may suit users with multiple copies of documents across different devices and directories."), metadata_folder_str.hash), +} +local metadata_folder_help_text = table.concat(metadata_folder_help_table, "\n") -local hash_filemod_warn = T(_([[%1 requires calculating partial file hashes of documents which may slow down file browser navigation. Any file modifications (such as embedding annotations into PDF files or downloading from calibre) may change the partial hash, thereby losing track of any highlights, bookmarks, and progress data. Embedding PDF annotations is currently set to "%s" and can be disabled at (⚙ → Document → Save Document (write highlights into PDF)).]]), DataStorage:getDocSettingsHashDir()) -local leaving_hash_sdr_warn = T(_("Warning: You currently have documents with hash-based metadata. Until this metadata is moved by opening those documents, or deleted, file browser navigation may remain slower.")) -local hash_metadata_file_list_header = T(_([[ -Hash-based metadata has been saved in %1 for the following documents. Hash-based storage may slow down file browser navigation in large directories. Thus, if not using hash-based metadata storage, it is recommended to open the associated documents in KOReader to automatically migrate their metadata to the preferred storage location, or to delete %1, which will speed up file browser navigation.]]), DataStorage:getDocSettingsHashDir()) +local hash_filemod_warn = T(_("%1 requires calculating partial file hashes of documents which may slow down file browser navigation. Any file modifications (such as embedding annotations into PDF files or downloading from calibre) may change the partial hash, thereby losing track of any highlights, bookmarks, and progress data. Embedding PDF annotations is currently set to \"%s\" and can be disabled at (⚙ → Document → Save Document (write highlights into PDF))."), metadata_folder_str.hash) +local leaving_hash_sdr_warn = _("Warning: You currently have documents with hash-based metadata. Until this metadata is moved by opening those documents, or deleted, file browser navigation may remain slower.") local function genMetadataFolderMenuItem(value) return { @@ -574,7 +574,7 @@ local function genMetadataFolderMenuItem(value) local save_document_setting = G_reader_settings:readSetting("save_document") UIManager:show(InfoMessage:new{ text = string.format(hash_filemod_warn, save_document_setting), icon = "notice-warning" }) else - DocSettings.setIsHashLocationEnabled(nil) -- setting to nil will let it reset itself appropriately + DocSettings.setIsHashLocationEnabled(nil) -- reset if DocSettings.isHashLocationEnabled() then UIManager:show(InfoMessage:new{ text = leaving_hash_sdr_warn, icon = "notice-warning" }) end @@ -604,7 +604,7 @@ common_settings.document_metadata_location = { genMetadataFolderMenuItem("doc"), genMetadataFolderMenuItem("dir"), genMetadataFolderMenuItem("hash"), - { -- hash-based metadata count / TextViewer + { text_func = function() local hash_text = _("Show documents with hash-based metadata") local no_hash_text = _("No documents with hash-based metadata") @@ -621,18 +621,7 @@ common_settings.document_metadata_location = { return DocSettings.isHashLocationEnabled() end, callback = function() - local hash_file_infos = DocSettings.getHashDirSdrInfos() - local book_info_items = {} - for i, file_info in ipairs(hash_file_infos) do - table.insert(book_info_items, table.concat({"\n", i, ". ", file_info})) - end - local book_info_str = table.concat(book_info_items) - UIManager:show(require("ui/widget/textviewer"):new{ - title = T(N_("1 document with hash-based metadata", "%1 documents with hash-based metadata", #hash_file_infos), #hash_file_infos), - title_multilines = true, - justified = false, - text = hash_metadata_file_list_header .. book_info_str, - }) + FileManagerBookInfo.showBooksWithHashBasedMetadata() end, }, }, diff --git a/spec/unit/docsettings_spec.lua b/spec/unit/docsettings_spec.lua index e76070ff9..8518a3b2b 100644 --- a/spec/unit/docsettings_spec.lua +++ b/spec/unit/docsettings_spec.lua @@ -1,5 +1,8 @@ describe("docsettings module", function() local DataStorage, docsettings, docsettings_dir, ffiutil, lfs + local getSidecarFile = function(doc_path) + return docsettings:getSidecarDir(doc_path).."/"..docsettings.getSidecarFilename(doc_path) + end setup(function() require("commonrequire") @@ -33,20 +36,15 @@ describe("docsettings module", function() it("should generate sidecar metadata file (book folder)", function() G_reader_settings:saveSetting("document_metadata_folder", "doc") - assert.Equals("../../foo.sdr/metadata.pdf.lua", - docsettings:getSidecarFile("../../foo.pdf")) - assert.Equals("/foo/bar.sdr/metadata.pdf.lua", - docsettings:getSidecarFile("/foo/bar.pdf")) - assert.Equals("baz.sdr/metadata.epub.lua", - docsettings:getSidecarFile("baz.epub")) + assert.Equals("../../foo.sdr/metadata.pdf.lua", getSidecarFile("../../foo.pdf")) + assert.Equals("/foo/bar.sdr/metadata.pdf.lua", getSidecarFile("/foo/bar.pdf")) + assert.Equals("baz.sdr/metadata.epub.lua", getSidecarFile("baz.epub")) end) it("should generate sidecar metadata file (docsettings folder)", function() G_reader_settings:saveSetting("document_metadata_folder", "dir") - assert.Equals(docsettings_dir.."/foo/bar.sdr/metadata.pdf.lua", - docsettings:getSidecarFile("/foo/bar.pdf")) - assert.Equals(docsettings_dir.."baz.sdr/metadata.epub.lua", - docsettings:getSidecarFile("baz.epub")) + assert.Equals(docsettings_dir.."/foo/bar.sdr/metadata.pdf.lua", getSidecarFile("/foo/bar.pdf")) + assert.Equals(docsettings_dir.."baz.sdr/metadata.epub.lua", getSidecarFile("baz.epub")) end) it("should read legacy history file", function() @@ -65,9 +63,9 @@ describe("docsettings module", function() } for _, f in ipairs(legacy_files) do - assert.False(os.rename(d.doc_sidecar_file, f) == nil) + assert.False(os.rename(d.doc_sidecar_dir.."/"..d.sidecar_filename, f) == nil) d = docsettings:open(file) - assert.True(os.remove(d.doc_sidecar_file) == nil) + assert.True(os.remove(d.doc_sidecar_dir.."/"..d.sidecar_filename) == nil) -- Legacy history files should not be removed before flush has been -- called. assert.Equals(lfs.attributes(f, "mode"), "file") @@ -80,7 +78,7 @@ describe("docsettings module", function() assert.True(os.remove(f) == nil) end - assert.False(os.remove(d.doc_sidecar_file) == nil) + assert.False(os.remove(d.doc_sidecar_dir.."/"..d.sidecar_filename) == nil) d:purge() end) @@ -98,7 +96,7 @@ describe("docsettings module", function() for i, v in ipairs(legacy_files) do d:saveSetting("a", i) d:flush() - assert.False(os.rename(d.doc_sidecar_file, v.."1") == nil) + assert.False(os.rename(d.doc_sidecar_dir.."/"..d.sidecar_filename, v.."1") == nil) end d:close() @@ -127,33 +125,33 @@ describe("docsettings module", function() d:saveSetting("a", "a") d:flush() -- metadata.pdf.lua should be generated. - assert.Equals("file", lfs.attributes(d.doc_sidecar_file, "mode")) + assert.Equals("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename, "mode")) d:flush() -- metadata.pdf.lua.old should not yet be generated. - assert.are.not_equal("file", lfs.attributes(d.doc_sidecar_file .. ".old", "mode")) + assert.are.not_equal("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename .. ".old", "mode")) -- make metadata.pdf.lua older to bypass 60s age needed for .old rotation local minutes_ago = os.time() - 120 - lfs.touch(d.doc_sidecar_file, minutes_ago) + lfs.touch(d.doc_sidecar_dir.."/"..d.sidecar_filename, minutes_ago) d:close() -- metadata.pdf.lua and metadata.pdf.lua.old should be generated. - assert.Equals("file", lfs.attributes(d.doc_sidecar_file, "mode")) - assert.Equals("file", lfs.attributes(d.doc_sidecar_file .. ".old", "mode")) + assert.Equals("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename, "mode")) + assert.Equals("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename .. ".old", "mode")) -- write some garbage to sidecar-file. - local f_out = io.open(d.doc_sidecar_file, "w") + local f_out = io.open(d.doc_sidecar_dir.."/"..d.sidecar_filename, "w") f_out:write("bla bla bla") f_out:close() d = docsettings:open(file) -- metadata.pdf.lua should be removed. - assert.are.not_equal("file", lfs.attributes(d.doc_sidecar_file, "mode")) - assert.Equals("file", lfs.attributes(d.doc_sidecar_file .. ".old", "mode")) + assert.are.not_equal("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename, "mode")) + assert.Equals("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename .. ".old", "mode")) assert.Equals("a", d:readSetting("a")) d:saveSetting("a", "b") d:close() -- metadata.pdf.lua should be generated. - assert.Equals("file", lfs.attributes(d.doc_sidecar_file, "mode")) - assert.Equals("file", lfs.attributes(d.doc_sidecar_file .. ".old", "mode")) + assert.Equals("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename, "mode")) + assert.Equals("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename .. ".old", "mode")) -- The contents in sidecar_file and sidecar_file.old are different. -- a:b v.s. a:a @@ -161,21 +159,21 @@ describe("docsettings module", function() -- The content should come from sidecar_file. assert.Equals("b", d:readSetting("a")) -- write some garbage to sidecar-file. - f_out = io.open(d.doc_sidecar_file, "w") + f_out = io.open(d.doc_sidecar_dir.."/"..d.sidecar_filename, "w") f_out:write("bla bla bla") f_out:close() -- do not flush the result, open docsettings again. d = docsettings:open(file) -- metadata.pdf.lua should be removed. - assert.are.not_equal("file", lfs.attributes(d.doc_sidecar_file, "mode")) - assert.Equals("file", lfs.attributes(d.doc_sidecar_file .. ".old", "mode")) + assert.are.not_equal("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename, "mode")) + assert.Equals("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename .. ".old", "mode")) -- The content should come from sidecar_file.old. assert.Equals("a", d:readSetting("a")) d:close() -- metadata.pdf.lua should be generated. - assert.Equals("file", lfs.attributes(d.doc_sidecar_file, "mode")) - assert.Equals("file", lfs.attributes(d.doc_sidecar_file .. ".old", "mode")) + assert.Equals("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename, "mode")) + assert.Equals("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename .. ".old", "mode")) end) describe("ignore empty sidecar file", function() @@ -186,29 +184,29 @@ describe("docsettings module", function() d:saveSetting("a", "a") d:flush() -- metadata.pdf.lua should be generated. - assert.Equals("file", lfs.attributes(d.doc_sidecar_file, "mode")) + assert.Equals("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename, "mode")) -- make metadata.pdf.lua older to bypass 60s age needed for .old rotation local minutes_ago = os.time() - 120 - lfs.touch(d.doc_sidecar_file, minutes_ago) + lfs.touch(d.doc_sidecar_dir.."/"..d.sidecar_filename, minutes_ago) d:close() -- metadata.pdf.lua and metadata.pdf.lua.old should be generated. - assert.Equals("file", lfs.attributes(d.doc_sidecar_file, "mode")) - assert.Equals("file", lfs.attributes(d.doc_sidecar_file .. ".old", "mode")) + assert.Equals("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename, "mode")) + assert.Equals("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename .. ".old", "mode")) -- reset the sidecar_file to an empty file. - local f_out = io.open(d.doc_sidecar_file, "w") + local f_out = io.open(d.doc_sidecar_dir.."/"..d.sidecar_filename, "w") f_out:close() d = docsettings:open(file) -- metadata.pdf.lua should be removed. - assert.are.not_equal("file", lfs.attributes(d.doc_sidecar_file, "mode")) - assert.Equals("file", lfs.attributes(d.doc_sidecar_file .. ".old", "mode")) + assert.are.not_equal("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename, "mode")) + assert.Equals("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename .. ".old", "mode")) assert.Equals("a", d:readSetting("a")) d:saveSetting("a", "b") d:close() -- metadata.pdf.lua should be generated. - assert.Equals("file", lfs.attributes(d.doc_sidecar_file, "mode")) - assert.Equals("file", lfs.attributes(d.doc_sidecar_file .. ".old", "mode")) + assert.Equals("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename, "mode")) + assert.Equals("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename .. ".old", "mode")) -- The contents in sidecar_file and sidecar_file.old are different. -- a:b v.s. a:a end) @@ -220,30 +218,30 @@ describe("docsettings module", function() d:saveSetting("a", "a") d:flush() -- metadata.pdf.lua should be generated. - assert.Equals("file", lfs.attributes(d.doc_sidecar_file, "mode")) + assert.Equals("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename, "mode")) -- make metadata.pdf.lua older to bypass 60s age needed for .old rotation local minutes_ago = os.time() - 120 - lfs.touch(d.doc_sidecar_file, minutes_ago) + lfs.touch(d.doc_sidecar_dir.."/"..d.sidecar_filename, minutes_ago) d:close() -- metadata.pdf.lua and metadata.pdf.lua.old should be generated. - assert.Equals("file", lfs.attributes(d.doc_sidecar_file, "mode")) - assert.Equals("file", lfs.attributes(d.doc_sidecar_file .. ".old", "mode")) + assert.Equals("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename, "mode")) + assert.Equals("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename .. ".old", "mode")) -- reset the sidecar_file to an empty file. - local f_out = io.open(d.doc_sidecar_file, "w") + local f_out = io.open(d.doc_sidecar_dir.."/"..d.sidecar_filename, "w") f_out:write("{ } ") f_out:close() d = docsettings:open(file) -- metadata.pdf.lua should be removed. - assert.are.not_equal("file", lfs.attributes(d.doc_sidecar_file, "mode")) - assert.Equals("file", lfs.attributes(d.doc_sidecar_file .. ".old", "mode")) + assert.are.not_equal("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename, "mode")) + assert.Equals("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename .. ".old", "mode")) assert.Equals("a", d:readSetting("a")) d:saveSetting("a", "b") d:close() -- metadata.pdf.lua should be generated. - assert.Equals("file", lfs.attributes(d.doc_sidecar_file, "mode")) - assert.Equals("file", lfs.attributes(d.doc_sidecar_file .. ".old", "mode")) + assert.Equals("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename, "mode")) + assert.Equals("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename .. ".old", "mode")) -- The contents in sidecar_file and sidecar_file.old are different. -- a:b v.s. a:a end) diff --git a/spec/unit/filemanager_spec.lua b/spec/unit/filemanager_spec.lua index e6a511ff8..ce46b6399 100644 --- a/spec/unit/filemanager_spec.lua +++ b/spec/unit/filemanager_spec.lua @@ -47,7 +47,7 @@ describe("FileManager module", function() local tmp_sidecar = docsettings:getSidecarDir(util.realpath(tmp_fn)) lfs.mkdir(tmp_sidecar) - local tmp_sidecar_file = docsettings:getSidecarFile(util.realpath(tmp_fn)) + local tmp_sidecar_file = docsettings:getSidecarDir(util.realpath(tmp_fn)).."/"..docsettings.getSidecarFilename(util.realpath(tmp_fn)) local tmp_sidecar_file_foo = tmp_sidecar_file .. ".foo" -- non-docsettings file local tmpsf = io.open(tmp_sidecar_file, "w") tmpsf:write("{}") @@ -85,7 +85,7 @@ describe("FileManager module", function() local tmp_sidecar = docsettings:getSidecarDir(util.realpath(tmp_fn)) lfs.mkdir(tmp_sidecar) - local tmp_sidecar_file = docsettings:getSidecarFile(util.realpath(tmp_fn)) + local tmp_sidecar_file = docsettings:getSidecarDir(util.realpath(tmp_fn)).."/"..docsettings.getSidecarFilename(util.realpath(tmp_fn)) local tmpsf = io.open(tmp_sidecar_file, "w") tmpsf:write("{}") tmpsf:close()