DocSettings (again) (#11020)

Cleaning and optimizing Docsettings code.
reviewable/pr11060/r1
hius07 6 months ago committed by GitHub
parent 873503369c
commit b70f866656
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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)

@ -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

@ -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)

@ -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

@ -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

@ -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

@ -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 <book-filename>.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 <book-filename>.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,
},
},

@ -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)

@ -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()

Loading…
Cancel
Save