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 DocumentRegistry = require("document/documentregistry")
local Event = require("ui/event") local Event = require("ui/event")
local FileChooser = require("ui/widget/filechooser") local FileChooser = require("ui/widget/filechooser")
local FileManagerBookInfo = require("apps/filemanager/filemanagerbookinfo")
local FileManagerCollection = require("apps/filemanager/filemanagercollection") local FileManagerCollection = require("apps/filemanager/filemanagercollection")
local FileManagerConverter = require("apps/filemanager/filemanagerconverter") local FileManagerConverter = require("apps/filemanager/filemanagerconverter")
local FileManagerFileSearcher = require("apps/filemanager/filemanagerfilesearcher") local FileManagerFileSearcher = require("apps/filemanager/filemanagerfilesearcher")
@ -415,6 +416,7 @@ function FileManager:init()
self:registerModule("menu", self.menu) self:registerModule("menu", self.menu)
self:registerModule("history", FileManagerHistory:new{ ui = self }) self:registerModule("history", FileManagerHistory:new{ ui = self })
self:registerModule("bookinfo", FileManagerBookInfo:new{ ui = self })
self:registerModule("collections", FileManagerCollection:new{ ui = self }) self:registerModule("collections", FileManagerCollection:new{ ui = self })
self:registerModule("filesearcher", FileManagerFileSearcher:new{ ui = self }) self:registerModule("filesearcher", FileManagerFileSearcher:new{ ui = self })
self:registerModule("folder_shortcuts", FileManagerShortcuts:new{ ui = self }) self:registerModule("folder_shortcuts", FileManagerShortcuts:new{ ui = self })
@ -879,7 +881,7 @@ function FileManager:pasteHere(file)
local function infoCopyFile() local function infoCopyFile()
if self:copyRecursive(orig_file, dest_path) then if self:copyRecursive(orig_file, dest_path) then
if is_file then if is_file then
DocSettings:updateLocation(orig_file, dest_file, true) DocSettings.updateLocation(orig_file, dest_file, true)
end end
return true return true
else else
@ -893,7 +895,7 @@ function FileManager:pasteHere(file)
local function infoMoveFile() local function infoMoveFile()
if self:moveFile(orig_file, dest_path) then if self:moveFile(orig_file, dest_path) then
if is_file 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) ReadHistory:updateItemByPath(orig_file, dest_file) -- (will update "lastfile" if needed)
else else
ReadHistory:updateItemsByPath(orig_file, dest_file) ReadHistory:updateItemsByPath(orig_file, dest_file)
@ -1018,7 +1020,7 @@ function FileManager:deleteFile(file, is_file)
end end
if ok and not err then if ok and not err then
if is_file then if is_file then
DocSettings:updateLocation(file) DocSettings.updateLocation(file)
ReadHistory:fileDeleted(file) ReadHistory:fileDeleted(file)
end end
ReadCollection:removeItemByPath(file, not is_file) ReadCollection:removeItemByPath(file, not is_file)
@ -1067,7 +1069,7 @@ function FileManager:renameFile(file, basename, is_file)
local function doRenameFile() local function doRenameFile()
if self:moveFile(file, dest) then if self:moveFile(file, dest) then
if is_file then if is_file then
DocSettings:updateLocation(file, dest) DocSettings.updateLocation(file, dest)
ReadHistory:updateItemByPath(file, dest) -- (will update "lastfile" if needed) ReadHistory:updateItemByPath(file, dest) -- (will update "lastfile" if needed)
else else
ReadHistory:updateItemsByPath(file, dest) ReadHistory:updateItemsByPath(file, dest)

@ -19,6 +19,8 @@ local filemanagerutil = require("apps/filemanager/filemanagerutil")
local lfs = require("libs/libkoreader-lfs") local lfs = require("libs/libkoreader-lfs")
local util = require("util") local util = require("util")
local _ = require("gettext") local _ = require("gettext")
local N_ = _.ngettext
local T = require("ffi/util").template
local BookInfo = WidgetContainer:extend{ local BookInfo = WidgetContainer:extend{
props = { props = {
@ -44,7 +46,7 @@ local BookInfo = WidgetContainer:extend{
} }
function BookInfo:init() function BookInfo:init()
if self.ui then -- only for Reader menu if self.document then -- only for Reader menu
self.ui.menu:registerToMainMenu(self) self.ui.menu:registerToMainMenu(self)
end end
end end
@ -83,7 +85,7 @@ function BookInfo:show(file, book_props)
book_props = BookInfo.getDocProps(file, book_props) book_props = BookInfo.getDocProps(file, book_props)
end end
-- cover image -- cover image
self.custom_book_cover = DocSettings:findCoverFile(file) self.custom_book_cover = DocSettings:findCustomCoverFile(file)
local key_text = self.prop_text["cover"] local key_text = self.prop_text["cover"]
if self.custom_book_cover then if self.custom_book_cover then
key_text = "\u{F040} " .. key_text key_text = "\u{F040} " .. key_text
@ -99,9 +101,9 @@ function BookInfo:show(file, book_props)
}) })
-- metadata -- metadata
local custom_props local custom_props
local custom_metadata_file = DocSettings:getCustomMetadataFile(file) local custom_metadata_file = DocSettings:findCustomMetadataFile(file)
if custom_metadata_file then 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") custom_props = self.custom_doc_settings:readSetting("custom_props")
end end
local values_lang local values_lang
@ -173,17 +175,17 @@ function BookInfo:show(file, book_props)
end end
function BookInfo.getCustomProp(prop_key, filepath) function BookInfo.getCustomProp(prop_key, filepath)
local custom_metadata_file = DocSettings:getCustomMetadataFile(filepath) local custom_metadata_file = DocSettings:findCustomMetadataFile(filepath)
return custom_metadata_file 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 end
-- Returns extended and customized metadata. -- Returns extended and customized metadata.
function BookInfo.extendProps(original_props, filepath) function BookInfo.extendProps(original_props, filepath)
-- do not customize if filepath is not passed (eg from covermenu) -- 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 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 {} original_props = original_props or {}
local props = {} 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"), -- If still no book_props (book never opened or empty "stats"),
-- but custom metadata exists, it has a copy of original doc_props -- but custom metadata exists, it has a copy of original doc_props
if not book_props then if not book_props then
local custom_metadata_file = DocSettings:getCustomMetadataFile(file) local custom_metadata_file = DocSettings:findCustomMetadataFile(file)
if custom_metadata_file then 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
end end
@ -320,7 +322,7 @@ function BookInfo:getCoverImage(doc, file, force_orig)
local cover_bb local cover_bb
-- check for a custom cover (orig cover is forcibly requested in "Book information" only) -- check for a custom cover (orig cover is forcibly requested in "Book information" only)
if not force_orig then 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 if custom_cover then
local cover_doc = DocumentRegistry:openDocument(custom_cover) local cover_doc = DocumentRegistry:openDocument(custom_cover)
if cover_doc then if cover_doc then
@ -348,8 +350,8 @@ function BookInfo:getCoverImage(doc, file, force_orig)
end end
function BookInfo:updateBookInfo(file, book_props, prop_updated, prop_value_old) function BookInfo:updateBookInfo(file, book_props, prop_updated, prop_value_old)
if prop_updated == "cover" and self.ui then if self.document and prop_updated == "cover" then
self.ui.doc_settings:getCoverFile(true) -- reset cover file cache self.ui.doc_settings:getCustomCoverFile(true) -- reset cover file cache
end end
self.prop_updated = { self.prop_updated = {
filepath = file, filepath = file,
@ -361,10 +363,10 @@ function BookInfo:updateBookInfo(file, book_props, prop_updated, prop_value_old)
self:show(file, book_props) self:show(file, book_props)
end end
function BookInfo:setCustomBookCover(file, book_props) function BookInfo:setCustomCover(file, book_props)
if self.custom_book_cover then -- reset custom cover if self.custom_book_cover then -- reset custom cover
if os.remove(self.custom_book_cover) then 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") self:updateBookInfo(file, book_props, "cover")
end end
else -- choose an image and set custom cover else -- choose an image and set custom cover
@ -386,28 +388,27 @@ end
function BookInfo:setCustomMetadata(file, book_props, prop_key, prop_value) function BookInfo:setCustomMetadata(file, book_props, prop_key, prop_value)
-- in file -- 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 if self.custom_doc_settings then
custom_doc_settings = self.custom_doc_settings custom_doc_settings = self.custom_doc_settings
custom_props = custom_doc_settings:readSetting("custom_props")
else -- no custom metadata file, create new else -- no custom metadata file, create new
custom_doc_settings = DocSettings:openCustomMetadata() custom_doc_settings = DocSettings.openSettingsFile()
custom_props = {}
display_title = book_props.display_title -- backup display_title = book_props.display_title -- backup
book_props.display_title = nil book_props.display_title = nil
custom_doc_settings:saveSetting("doc_props", book_props) -- save a copy of original props custom_doc_settings:saveSetting("doc_props", book_props) -- save a copy of original props
end end
custom_props = custom_doc_settings:readSetting("custom_props", {})
local prop_value_old = custom_props[prop_key] or book_props[prop_key] 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 custom_props[prop_key] = prop_value -- nil when resetting a custom prop
if next(custom_props) == nil then -- no more custom metadata if next(custom_props) == nil then -- no more custom metadata
os.remove(custom_doc_settings.custom_metadata_file) os.remove(custom_doc_settings.sidecar_file)
DocSettings:removeSidecarDir(file, util.splitFilePathName(custom_doc_settings.custom_metadata_file)) DocSettings.removeSidecarDir(util.splitFilePathName(custom_doc_settings.sidecar_file))
no_custom_metadata = true
else else
if book_props.pages then -- keep a copy of original 'pages' up to date if book_props.pages then -- keep a copy of original 'pages' up to date
local original_props = custom_doc_settings:readSetting("doc_props") local original_props = custom_doc_settings:readSetting("doc_props")
original_props.pages = book_props.pages original_props.pages = book_props.pages
end end
custom_doc_settings:saveSetting("custom_props", custom_props)
custom_doc_settings:flushCustomMetadata(file) custom_doc_settings:flushCustomMetadata(file)
end end
book_props.display_title = book_props.display_title or display_title -- restore 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 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) book_props.display_title = book_props.title or filemanagerutil.splitFileNameType(file)
end end
local ui = self.ui or require("apps/reader/readerui").instance if self.document and self.document.file == file then -- currently opened document
if ui and ui.document and ui.document.file == file then -- currently opened document self.ui.doc_props[prop_key] = prop_value
ui.doc_props[prop_key] = prop_value
if prop_key == "title" then 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
end end
self:updateBookInfo(file, book_props, prop_key, prop_value_old) 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() ok_callback = function()
UIManager:close(button_dialog) UIManager:close(button_dialog)
if prop_is_cover then if prop_is_cover then
self:setCustomBookCover(file, book_props) self:setCustomCover(file, book_props)
else else
self:setCustomMetadata(file, book_props, prop_key) self:setCustomMetadata(file, book_props, prop_key)
end end
@ -532,7 +535,7 @@ function BookInfo:showCustomDialog(file, book_props, prop_key)
callback = function() callback = function()
UIManager:close(button_dialog) UIManager:close(button_dialog)
if prop_is_cover then if prop_is_cover then
self:setCustomBookCover(file, book_props) self:setCustomCover(file, book_props)
else else
self:showCustomEditDialog(file, book_props, prop_key) self:showCustomEditDialog(file, book_props, prop_key)
end end
@ -548,4 +551,98 @@ function BookInfo:showCustomDialog(file, book_props, prop_key)
UIManager:show(button_dialog) UIManager:show(button_dialog)
end 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 return BookInfo

@ -15,7 +15,6 @@ local lfs = require("libs/libkoreader-lfs")
local logger = require("logger") local logger = require("logger")
local util = require("util") local util = require("util")
local _ = require("gettext") local _ = require("gettext")
local N_ = _.ngettext
local T = FFIUtil.template local T = FFIUtil.template
local FileManagerMenu = InputContainer:extend{ local FileManagerMenu = InputContainer:extend{
@ -489,7 +488,7 @@ To:
text = _("Move book metadata"), text = _("Move book metadata"),
keep_menu_open = true, keep_menu_open = true,
callback = function() callback = function()
self:moveBookMetadata() self.ui.bookinfo:moveBookMetadata()
end, end,
} }
@ -930,74 +929,6 @@ function FileManagerMenu:getStartWithMenuTable()
} }
end 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) function FileManagerMenu:exitOrRestart(callback, force)
UIManager:close(self.menu_container) UIManager:close(self.menu_container)

@ -142,9 +142,9 @@ end
function filemanagerutil.genResetSettingsButton(file, caller_callback, button_disabled) function filemanagerutil.genResetSettingsButton(file, caller_callback, button_disabled)
file = ffiutil.realpath(file) or file file = ffiutil.realpath(file) or file
local has_sidecar_file = DocSettings:hasSidecarFile(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 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 local has_custom_metadata_file = custom_metadata_file and true or false
return { return {
text = _("Reset"), text = _("Reset"),
@ -162,8 +162,8 @@ function filemanagerutil.genResetSettingsButton(file, caller_callback, button_di
ok_callback = function() ok_callback = function()
local data_to_purge = { local data_to_purge = {
doc_settings = check_button_settings.checked, doc_settings = check_button_settings.checked,
custom_cover_file = check_button_cover.checked, custom_cover_file = check_button_cover.checked and custom_cover_file,
custom_metadata_file = check_button_metadata.checked, custom_metadata_file = check_button_metadata.checked and custom_metadata_file,
} }
DocSettings:open(file):purge(nil, data_to_purge) DocSettings:open(file):purge(nil, data_to_purge)
if data_to_purge.custom_cover_file or data_to_purge.custom_metadata_file then 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 DOCSETTINGS_HASH_DIR = DataStorage:getDocSettingsHashDir()
local custom_metadata_filename = "custom_metadata.lua" 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 doc_hash_cache = {}
local is_hash_location_enabled
function DocSettings.isHashLocationEnabled() function DocSettings.isHashLocationEnabled()
if is_hash_location_enabled == nil then 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 end
return is_hash_location_enabled return is_hash_location_enabled
end end
@ -33,14 +49,13 @@ function DocSettings.setIsHashLocationEnabled(value)
is_hash_location_enabled = value is_hash_location_enabled = value
end end
local function buildCandidates(list) local function buildCandidates(list)
local candidates = {} local candidates = {}
local previous_entry_exists = false local previous_entry_exists = false
for i, file_path in ipairs(list) do for i, file_path in ipairs(list) do
-- Ignore missing files. -- 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") local mtime = lfs.attributes(file_path, "modification")
-- NOTE: Extra trickery: if we're inserting a "backup" file, and its primary buddy exists, -- 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. -- make sure it will *never* sort ahead of it by using the same mtime.
@ -81,6 +96,18 @@ local function buildCandidates(list)
return candidates return candidates
end 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`). --- Returns path to sidecar directory (`filename.sdr`).
-- Sidecar directory is the file without _last_ suffix. -- Sidecar directory is the file without _last_ suffix.
-- @string doc_path path to the document (e.g., `/foo/bar.pdf`) -- @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" return path .. ".sdr"
end end
--- Returns path to `metadata.lua` file. function DocSettings.getSidecarFilename(doc_path)
-- @string doc_path path to the document (e.g., `/foo/bar.pdf`) local suffix = doc_path:match(".*%.(.+)") or "_"
-- @treturn string path to `/foo/bar.sdr/metadata.lua` file return "metadata." .. suffix .. ".lua"
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"
end end
--- Returns `true` if there is a `metadata.lua` file. --- Returns `true` if there is a `metadata.lua` file.
-- @string doc_path path to the document (e.g., `/foo/bar.pdf`) -- @string doc_path path to the document (e.g., `/foo/bar.pdf`)
-- @treturn bool -- @treturn bool
function DocSettings:hasSidecarFile(doc_path) function DocSettings:hasSidecarFile(doc_path)
return self:getDocSidecarFile(doc_path) and true or false return self:findSidecarFile(doc_path) and true or false
end end
--- Returns path of `metadata.lua` file if it exists, or nil. --- Returns path of `metadata.lua` file if it exists, or nil.
-- @string doc_path path to the document (e.g., `/foo/bar.pdf`) -- @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 -- @bool no_legacy set to true to skip check of the legacy history file
-- @treturn string -- @treturn string
function DocSettings:getDocSidecarFile(doc_path, no_legacy) function DocSettings:findSidecarFile(doc_path, no_legacy)
local sidecar_file = self:getSidecarFile(doc_path, "doc") local sidecar_filename = DocSettings.getSidecarFilename(doc_path)
if lfs.attributes(sidecar_file, "mode") == "file" then local sidecar_file
return sidecar_file for _, location in ipairs(getOrderedLocationCandidates()) do
end sidecar_file = self:getSidecarDir(doc_path, location) .. "/" .. sidecar_filename
sidecar_file = self:getSidecarFile(doc_path, "dir") if isFile(sidecar_file) then
if lfs.attributes(sidecar_file, "mode") == "file" then return sidecar_file, location
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
end end
end end
if not no_legacy then if not no_legacy then
sidecar_file = self:getHistoryPath(doc_path) sidecar_file = self:getHistoryPath(doc_path)
if lfs.attributes(sidecar_file, "mode") == "file" then if isFile(sidecar_file) then
return sidecar_file return sidecar_file, "hist" -- for isSidecarFileNotInPreferredLocation() used in moveBookMetadata
end end
end 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) function DocSettings:getHistoryPath(doc_path)
if doc_path == nil or doc_path == "" then return "" end if doc_path == nil or doc_path == "" then return "" end
return HISTORY_DIR .. "/[" .. doc_path:gsub("(.*/)([^/]+)", "%1] %2"):gsub("/", "#") .. ".lua" return HISTORY_DIR .. "/[" .. doc_path:gsub("(.*/)([^/]+)", "%1] %2"):gsub("/", "#") .. ".lua"
@ -193,20 +212,6 @@ function DocSettings:getFileFromHistory(hist_name)
end end
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.) --- Opens a document's individual settings (font, margin, dictionary, etc.)
-- @string doc_path path to the document (e.g., `/foo/bar.pdf`) -- @string doc_path path to the document (e.g., `/foo/bar.pdf`)
-- @treturn DocSettings object -- @treturn DocSettings object
@ -214,28 +219,25 @@ function DocSettings:open(doc_path)
-- NOTE: Beware, our new instance is new, but self is still DocSettings! -- NOTE: Beware, our new instance is new, but self is still DocSettings!
local new = DocSettings:extend{} local new = DocSettings:extend{}
new.sidecar_filename = DocSettings.getSidecarFilename(doc_path)
new.doc_sidecar_dir = new:getSidecarDir(doc_path, "doc") 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 local doc_sidecar_file, legacy_sidecar_file
if lfs.attributes(new.doc_sidecar_dir, "mode") == "directory" then if isDir(new.doc_sidecar_dir) then
doc_sidecar_file = new.doc_sidecar_file doc_sidecar_file = new.doc_sidecar_dir .. "/" .. new.sidecar_filename
legacy_sidecar_file = new.doc_sidecar_dir .. "/" .. ffiutil.basename(doc_path) .. ".lua" legacy_sidecar_file = new.doc_sidecar_dir .. "/" .. ffiutil.basename(doc_path) .. ".lua"
end end
new.dir_sidecar_dir = new:getSidecarDir(doc_path, "dir") new.dir_sidecar_dir = new:getSidecarDir(doc_path, "dir")
new.dir_sidecar_file = new:getSidecarFile(doc_path, "dir")
local dir_sidecar_file local dir_sidecar_file
if lfs.attributes(new.dir_sidecar_dir, "mode") == "directory" then if isDir(new.dir_sidecar_dir) then
dir_sidecar_file = new.dir_sidecar_file dir_sidecar_file = new.dir_sidecar_dir .. "/" .. new.sidecar_filename
end end
local history_file = new:getHistoryPath(doc_path) local hash_sidecar_file
local hash_sidecar_dir, hash_sidecar_file
if DocSettings.isHashLocationEnabled() then if DocSettings.isHashLocationEnabled() then
hash_sidecar_dir, hash_sidecar_file = new.hash_sidecar_dir = new:getSidecarDir(doc_path, "hash")
new:getSidecarHashDirAndFilepath(doc_path) hash_sidecar_file = new.hash_sidecar_dir .. "/" .. new.sidecar_filename
new.hash_sidecar_dir = hash_sidecar_dir
new.hash_sidecar_file = hash_sidecar_file
end end
local history_file = new:getHistoryPath(doc_path)
-- Candidates list, in order of priority: -- Candidates list, in order of priority:
local candidates_list = { local candidates_list = {
@ -249,10 +251,10 @@ function DocSettings:open(doc_path)
dir_sidecar_file or "", dir_sidecar_file or "",
-- Backup file of new sidecar file in docsettings folder -- Backup file of new sidecar file in docsettings folder
dir_sidecar_file and (dir_sidecar_file .. ".old") or "", 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 "", hash_sidecar_file or "",
-- Backup file of hash or PDF fingerprint-based sidecar file lookup -- Backup file of new sidecar file in hashdocsettings folder
hash_sidecar_file and (new.hash_sidecar_file .. ".old") or "", hash_sidecar_file and (hash_sidecar_file .. ".old") or "",
-- Legacy history folder -- Legacy history folder
history_file, history_file,
-- Backup file in legacy history folder -- Backup file in legacy history folder
@ -290,52 +292,69 @@ function DocSettings:open(doc_path)
return new return new
end 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`. --- Serializes settings and writes them to `metadata.lua`.
function DocSettings:flush(data, no_custom_metadata) function DocSettings:flush(data, no_custom_metadata)
-- Depending on the settings, doc_settings are saved to the book folder or data = data or self.data
-- to koreader/docsettings folder. The latter is also a fallback for read-only book storage. local sidecar_dirs
local serials local preferred_location = G_reader_settings:readSetting("document_metadata_folder", "doc")
local preferred_metdata_storage = G_reader_settings:readSetting("document_metadata_folder", "doc") if preferred_location == "doc" then
if preferred_metdata_storage == "doc" then sidecar_dirs = { self.doc_sidecar_dir, self.dir_sidecar_dir } -- fallback for read-only book storage
serials = { {self.doc_sidecar_dir, self.doc_sidecar_file}, elseif preferred_location == "dir" then
{self.dir_sidecar_dir, self.dir_sidecar_file}, } sidecar_dirs = { self.dir_sidecar_dir }
elseif preferred_metdata_storage == "dir" then elseif preferred_location == "hash" then
serials = { {self.dir_sidecar_dir, self.dir_sidecar_file}, } if self.hash_sidecar_dir == nil then
elseif preferred_metdata_storage == "hash" then self.hash_sidecar_dir = self:getSidecarDir(data.doc_path, "hash")
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)
end end
serials = { {self.hash_sidecar_dir, self.hash_sidecar_file } } sidecar_dirs = { self.hash_sidecar_dir }
end end
local s_out = dump(data or self.data, nil, true) local ser_data = dump(data, nil, true)
for _, s in ipairs(serials) do for _, sidecar_dir in ipairs(sidecar_dirs) do
local sidecar_dir, sidecar_file = unpack(s) local sidecar_dir_slash = sidecar_dir .. "/"
local sidecar_file = sidecar_dir_slash .. self.sidecar_filename
util.makePath(sidecar_dir) util.makePath(sidecar_dir)
logger.dbg("DocSettings: Writing to", sidecar_file) logger.dbg("DocSettings: Writing to", sidecar_file)
local directory_updated = LuaSettings:backup(sidecar_file) local directory_updated = LuaSettings:backup(sidecar_file) -- "*.old"
if util.writeToFile(s_out, sidecar_file, true, true, directory_updated) then 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 -- move custom cover file and custom metadata file to the metadata file location
if not no_custom_metadata then if not no_custom_metadata then
local metadata_file, filepath, filename local metadata_file, filepath, filename
-- custom cover -- custom cover
metadata_file = self:getCoverFile() metadata_file = self:getCustomCoverFile()
if metadata_file then if metadata_file then
filepath, filename = util.splitFilePathName(metadata_file) filepath, filename = util.splitFilePathName(metadata_file)
if filepath ~= sidecar_dir .. "/" then if filepath ~= sidecar_dir_slash then
ffiutil.copyFile(metadata_file, sidecar_dir .. "/" .. filename) ffiutil.copyFile(metadata_file, sidecar_dir_slash .. filename)
os.remove(metadata_file) os.remove(metadata_file)
self:getCoverFile(true) -- reset cache self:getCustomCoverFile(true) -- reset cache
end end
end end
-- custom metadata -- custom metadata
metadata_file = self:getCustomMetadataFile() metadata_file = self:getCustomMetadataFile()
if metadata_file then if metadata_file then
filepath, filename = util.splitFilePathName(metadata_file) filepath, filename = util.splitFilePathName(metadata_file)
if filepath ~= sidecar_dir .. "/" then if filepath ~= sidecar_dir_slash then
ffiutil.copyFile(metadata_file, sidecar_dir .. "/" .. filename) ffiutil.copyFile(metadata_file, sidecar_dir_slash .. filename)
os.remove(metadata_file) os.remove(metadata_file)
self:getCustomMetadataFile(true) -- reset cache
end end
end end
end end
@ -351,10 +370,10 @@ end
function DocSettings:purge(sidecar_to_keep, data_to_purge) function DocSettings:purge(sidecar_to_keep, data_to_purge)
local custom_cover_file, custom_metadata_file local custom_cover_file, custom_metadata_file
if sidecar_to_keep == nil then if sidecar_to_keep == nil then
custom_cover_file = self:getCoverFile() custom_cover_file = self:getCustomCoverFile()
custom_metadata_file = self:getCustomMetadataFile() custom_metadata_file = self:getCustomMetadataFile()
end end
if data_to_purge == nil then if data_to_purge == nil then -- purge all
data_to_purge = { data_to_purge = {
doc_settings = true, doc_settings = true,
custom_cover_file = custom_cover_file, 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 if data_to_purge.doc_settings and self.candidates then
for _, t in ipairs(self.candidates) do for _, t in ipairs(self.candidates) do
local candidate_path = t.path local candidate_path = t.path
if lfs.attributes(candidate_path, "mode") == "file" then if isFile(candidate_path) then
if (not sidecar_to_keep) if (not sidecar_to_keep)
or (candidate_path ~= sidecar_to_keep and candidate_path ~= sidecar_to_keep .. ".old") then or (candidate_path ~= sidecar_to_keep and candidate_path ~= sidecar_to_keep .. ".old") then
os.remove(candidate_path) os.remove(candidate_path)
@ -376,105 +395,111 @@ function DocSettings:purge(sidecar_to_keep, data_to_purge)
end end
end end
-- Remove custom
if data_to_purge.custom_cover_file then if data_to_purge.custom_cover_file then
os.remove(data_to_purge.custom_cover_file) os.remove(data_to_purge.custom_cover_file)
self:getCoverFile(true) -- reset cache self:getCustomCoverFile(true) -- reset cache
end end
if data_to_purge.custom_metadata_file then if data_to_purge.custom_metadata_file then
os.remove(data_to_purge.custom_metadata_file) os.remove(data_to_purge.custom_metadata_file)
self:getCustomMetadataFile(true) -- reset cache
end 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 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 for _, dir in ipairs({ self.doc_sidecar_dir, self.dir_sidecar_dir, self.hash_sidecar_dir }) do
if lfs.attributes(self.doc_sidecar_dir, "mode") == "directory" then DocSettings.removeSidecarDir(dir)
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
end end
end end
DocSettings.setIsHashLocationEnabled(nil) -- reset this in case last hash book is purged DocSettings.setIsHashLocationEnabled(nil) -- reset this in case last hash book is purged
end end
--- Removes empty sidecar dir. --- Removes sidecar dir iff empty.
function DocSettings:removeSidecarDir(doc_path, sidecar_dir) function DocSettings.removeSidecarDir(dir)
if sidecar_dir == self:getSidecarDir(doc_path, "doc") then if dir and isDir(dir) then
os.remove(sidecar_dir) if dir:match("^"..DOCSETTINGS_DIR) or dir:match("^"..DOCSETTINGS_HASH_DIR) then
else util.removePath(dir) -- remove empty parent folders
util.removePath(sidecar_dir) else
os.remove(dir) -- keep parent folders
end
end end
end end
--- Updates sdr location for file rename/copy/move/delete operations. --- Updates sdr location for file rename/copy/move/delete operations.
function DocSettings:updateLocation(doc_path, new_doc_path, copy) function DocSettings.updateLocation(doc_path, new_doc_path, copy)
local doc_settings, new_sidecar_dir, cover_file local has_sidecar_file = DocSettings:hasSidecarFile(doc_path)
if G_reader_settings:readSetting("document_metadata_folder") == "hash" then local custom_cover_file = DocSettings:findCustomCoverFile(doc_path)
-- none of these operations (except delete) changes the hash -> no location change local custom_metadata_file = DocSettings:findCustomMetadataFile(doc_path)
if not new_doc_path then if not (has_sidecar_file or custom_cover_file or custom_metadata_file) then return end
doc_settings = DocSettings:open(doc_path)
local cache_file_path = doc_settings:readSetting("cache_file_path") local doc_settings = DocSettings:open(doc_path)
if cache_file_path then os.remove(cache_file_path) end local do_purge
cover_file = doc_settings:getCoverFile()
doc_settings:purge() if new_doc_path then -- copy/rename/move
end if G_reader_settings:readSetting("document_metadata_folder") ~= "hash" then -- keep hash location unchanged
else local new_sidecar_dir
-- update metadata if has_sidecar_file then
if DocSettings:hasSidecarFile(doc_path) then
doc_settings = DocSettings:open(doc_path)
if new_doc_path then
local new_doc_settings = DocSettings:open(new_doc_path) local new_doc_settings = DocSettings:open(new_doc_path)
-- save doc settings to the new location, no custom metadata yet doc_settings.data.doc_path = new_doc_path
new_sidecar_dir = new_doc_settings:flush(doc_settings.data, true) new_sidecar_dir = new_doc_settings:flush(doc_settings.data, true) -- without custom
else
local cache_file_path = doc_settings:readSetting("cache_file_path")
if cache_file_path then
os.remove(cache_file_path)
end
end end
end if not new_sidecar_dir then
new_sidecar_dir = DocSettings:getSidecarDir(new_doc_path)
-- update custom metadata util.makePath(new_sidecar_dir)
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)
end end
-- custom metadata if custom_cover_file then
local metadata_file = self:getCustomMetadataFile(doc_path) local _, filename = util.splitFilePathName(custom_cover_file)
if metadata_file then ffiutil.copyFile(custom_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
ffiutil.copyFile(metadata_file, new_sidecar_dir .. "/" .. custom_metadata_filename)
end end
if custom_metadata_file then
ffiutil.copyFile(custom_metadata_file, new_sidecar_dir .. "/" .. custom_metadata_filename)
end
do_purge = not copy
end end
else -- delete
if not copy then if has_sidecar_file then
doc_settings:purge() local cache_file_path = doc_settings:readSetting("cache_file_path")
if cache_file_path then
os.remove(cache_file_path)
end
end end
do_purge = true
end end
if cover_file then -- after purge because purge uses cover file cache if do_purge then
doc_settings:getCoverFile(true) -- reset cache 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 end
return { sidecar_dir }
end end
-- custom cover -- custom cover
local function findCoverFileInDir(dir) local function findCustomCoverFileInDir(dir)
local ok, iter, dir_obj = pcall(lfs.dir, dir) local ok, iter, dir_obj = pcall(lfs.dir, dir)
if ok then if ok then
for f in iter, dir_obj do for f in iter, dir_obj do
@ -486,57 +511,30 @@ local function findCoverFileInDir(dir)
end end
--- Returns path to book custom cover file if it exists, or nil. --- 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 doc_path = doc_path or self.data.doc_path
local location = G_reader_settings:readSetting("document_metadata_folder", "doc") for _, location in ipairs(getOrderedLocationCandidates()) do
local sidecar_dir = self:getSidecarDir(doc_path, location) local sidecar_dir = self:getSidecarDir(doc_path, location)
local cover_file = findCoverFileInDir(sidecar_dir) local custom_cover_file = findCustomCoverFileInDir(sidecar_dir)
if cover_file then return cover_file end if custom_cover_file then
local candidates = {"doc", "dir"} return custom_cover_file
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
end end
end end
end end
function DocSettings:getCoverFile(reset_cache) function DocSettings:getCustomCoverFile(reset_cache)
if reset_cache then if reset_cache then
self.cover_file = nil self.custom_cover_file = nil
else else
if self.cover_file == nil then -- fill empty cache if self.custom_cover_file == nil then -- fill empty cache
self.cover_file = self:findCoverFile() or false self.custom_cover_file = self:findCustomCoverFile() or false
end end
return self.cover_file return self.custom_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 }
end 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 end
function DocSettings:flushCustomCover(doc_path, image_file) 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() local new_cover_filename = "/cover." .. util.getFileNameSuffix(image_file):lower()
for _, sidecar_dir in ipairs(sidecar_dirs) do for _, sidecar_dir in ipairs(sidecar_dirs) do
util.makePath(sidecar_dir) util.makePath(sidecar_dir)
@ -550,130 +548,57 @@ end
-- custom metadata -- custom metadata
--- Returns path to book custom metadata file if it exists, or nil. --- 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 doc_path = doc_path or self.data.doc_path
for _, location in ipairs(getOrderedLocationCandidates()) do
local candidates = {"doc", "dir"} local sidecar_dir = self:getSidecarDir(doc_path, location)
if DocSettings.isHashLocationEnabled() then local custom_metadata_file = sidecar_dir .. "/" .. custom_metadata_filename
table.insert(candidates, "hash") if isFile(custom_metadata_file) then
end return custom_metadata_file
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
end end
end end
end end
function DocSettings:openCustomMetadata(custom_metadata_file) function DocSettings:getCustomMetadataFile(reset_cache)
local new = DocSettings:extend{} if reset_cache then
local ok, stored self.custom_metadata_file = nil
if custom_metadata_file then
ok, stored = pcall(dofile, custom_metadata_file)
end
if ok and next(stored) ~= nil then
new.data = stored
else 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 end
new.custom_metadata_file = custom_metadata_file
return new
end end
function DocSettings:flushCustomMetadata(doc_path) function DocSettings:flushCustomMetadata(doc_path)
local sidecar_dirs = self:getCustomCandidateSidecarDirs(doc_path) local sidecar_dirs = self:getCustomLocationCandidates(doc_path)
local new_sidecar_dir
local s_out = dump(self.data, nil, true) local s_out = dump(self.data, nil, true)
for _, sidecar_dir in ipairs(sidecar_dirs) do for _, sidecar_dir in ipairs(sidecar_dirs) do
util.makePath(sidecar_dir) util.makePath(sidecar_dir)
if util.writeToFile(s_out, sidecar_dir .. "/" .. custom_metadata_filename, true, true) then local new_metadata_file = sidecar_dir .. "/" .. custom_metadata_filename
new_sidecar_dir = sidecar_dir .. "/" if util.writeToFile(s_out, new_metadata_file, true, true) then
break return true
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
end end
end end
return sdrs
end end
function DocSettings.getHashDirSdrInfos() -- "hash" section
local sdrs = getSdrsInDir(DOCSETTINGS_HASH_DIR)
local title_author_strs = {} -- Returns the list of pairs {sidecar_file, custom_metadata_file}.
for _, sdr in ipairs(sdrs) do function DocSettings.findSidecarFilesInHashLocation()
-- Ignore empty files local res = {}
if lfs.attributes(sdr, "size") > 0 then local callback = function(fullpath, name)
local ok, stored if name:match("metadata%..+%.lua$") then
ok, stored = pcall(dofile, sdr) local sdr = { fullpath }
-- Ignore empty tables local custom_metadata_file = fullpath:gsub(name, custom_metadata_filename)
if ok and next(stored) ~= nil then if isFile(custom_metadata_file) then
local info_str, custom_authors table.insert(sdr, custom_metadata_file)
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)
end end
else table.insert(res, sdr)
table.insert(title_author_strs, "zero-size file " .. sdr)
end end
end end
return title_author_strs util.findFiles(DOCSETTINGS_HASH_DIR, callback)
return res
end end
return DocSettings return DocSettings

@ -149,7 +149,7 @@ function CreDocument:init()
self.flows = {} self.flows = {}
self.page_in_flow = {} 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 if file_type == "zip" then
-- NuPogodi, 20.05.12: read the content of zip-file -- NuPogodi, 20.05.12: read the content of zip-file
-- and return extention of the 1st file -- and return extention of the 1st file

@ -1,13 +1,13 @@
local DataStorage = require("datastorage")
local DateTimeWidget = require("ui/widget/datetimewidget") local DateTimeWidget = require("ui/widget/datetimewidget")
local Device = require("device") local Device = require("device")
local DocSettings = require("docsettings")
local Event = require("ui/event") local Event = require("ui/event")
local FileManagerBookInfo = require("apps/filemanager/filemanagerbookinfo")
local InfoMessage = require("ui/widget/infomessage") local InfoMessage = require("ui/widget/infomessage")
local Language = require("ui/language") local Language = require("ui/language")
local NetworkMgr = require("ui/network/manager") local NetworkMgr = require("ui/network/manager")
local PowerD = Device:getPowerDevice() local PowerD = Device:getPowerDevice()
local UIManager = require("ui/uimanager") local UIManager = require("ui/uimanager")
local DocSettings = require("docsettings")
local _ = require("gettext") local _ = require("gettext")
local N_ = _.ngettext local N_ = _.ngettext
local C_ = _.pgettext local C_ = _.pgettext
@ -541,23 +541,23 @@ common_settings.document = {
} }
local metadata_folder_str = { local metadata_folder_str = {
["doc"] = _("book folder"), ["doc"] = _("book folder"),
["dir"] = DataStorage:getDocSettingsDir(), ["dir"] = DocSettings.getSidecarStorage("dir"),
["hash"] = DataStorage:getDocSettingsHashDir() ["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"). 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:]])) "",
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.")) _("You can decide between three locations/methods where these will be saved:"),
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()) _(" - 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_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()) 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),
local metadata_folder_help_text = metadata_folder_help_header .. "\n" .. metadata_folder_help_doc .. "\n" .. metadata_folder_help_dir .. "\n" .. 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."), 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 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 = 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 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 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 function genMetadataFolderMenuItem(value) local function genMetadataFolderMenuItem(value)
return { return {
@ -574,7 +574,7 @@ local function genMetadataFolderMenuItem(value)
local save_document_setting = G_reader_settings:readSetting("save_document") 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" }) UIManager:show(InfoMessage:new{ text = string.format(hash_filemod_warn, save_document_setting), icon = "notice-warning" })
else else
DocSettings.setIsHashLocationEnabled(nil) -- setting to nil will let it reset itself appropriately DocSettings.setIsHashLocationEnabled(nil) -- reset
if DocSettings.isHashLocationEnabled() then if DocSettings.isHashLocationEnabled() then
UIManager:show(InfoMessage:new{ text = leaving_hash_sdr_warn, icon = "notice-warning" }) UIManager:show(InfoMessage:new{ text = leaving_hash_sdr_warn, icon = "notice-warning" })
end end
@ -604,7 +604,7 @@ common_settings.document_metadata_location = {
genMetadataFolderMenuItem("doc"), genMetadataFolderMenuItem("doc"),
genMetadataFolderMenuItem("dir"), genMetadataFolderMenuItem("dir"),
genMetadataFolderMenuItem("hash"), genMetadataFolderMenuItem("hash"),
{ -- hash-based metadata count / TextViewer {
text_func = function() text_func = function()
local hash_text = _("Show documents with hash-based metadata") local hash_text = _("Show documents with hash-based metadata")
local no_hash_text = _("No 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() return DocSettings.isHashLocationEnabled()
end, end,
callback = function() callback = function()
local hash_file_infos = DocSettings.getHashDirSdrInfos() FileManagerBookInfo.showBooksWithHashBasedMetadata()
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,
})
end, end,
}, },
}, },

@ -1,5 +1,8 @@
describe("docsettings module", function() describe("docsettings module", function()
local DataStorage, docsettings, docsettings_dir, ffiutil, lfs local DataStorage, docsettings, docsettings_dir, ffiutil, lfs
local getSidecarFile = function(doc_path)
return docsettings:getSidecarDir(doc_path).."/"..docsettings.getSidecarFilename(doc_path)
end
setup(function() setup(function()
require("commonrequire") require("commonrequire")
@ -33,20 +36,15 @@ describe("docsettings module", function()
it("should generate sidecar metadata file (book folder)", function() it("should generate sidecar metadata file (book folder)", function()
G_reader_settings:saveSetting("document_metadata_folder", "doc") G_reader_settings:saveSetting("document_metadata_folder", "doc")
assert.Equals("../../foo.sdr/metadata.pdf.lua", assert.Equals("../../foo.sdr/metadata.pdf.lua", getSidecarFile("../../foo.pdf"))
docsettings:getSidecarFile("../../foo.pdf")) assert.Equals("/foo/bar.sdr/metadata.pdf.lua", getSidecarFile("/foo/bar.pdf"))
assert.Equals("/foo/bar.sdr/metadata.pdf.lua", assert.Equals("baz.sdr/metadata.epub.lua", getSidecarFile("baz.epub"))
docsettings:getSidecarFile("/foo/bar.pdf"))
assert.Equals("baz.sdr/metadata.epub.lua",
docsettings:getSidecarFile("baz.epub"))
end) end)
it("should generate sidecar metadata file (docsettings folder)", function() it("should generate sidecar metadata file (docsettings folder)", function()
G_reader_settings:saveSetting("document_metadata_folder", "dir") G_reader_settings:saveSetting("document_metadata_folder", "dir")
assert.Equals(docsettings_dir.."/foo/bar.sdr/metadata.pdf.lua", assert.Equals(docsettings_dir.."/foo/bar.sdr/metadata.pdf.lua", getSidecarFile("/foo/bar.pdf"))
docsettings:getSidecarFile("/foo/bar.pdf")) assert.Equals(docsettings_dir.."baz.sdr/metadata.epub.lua", getSidecarFile("baz.epub"))
assert.Equals(docsettings_dir.."baz.sdr/metadata.epub.lua",
docsettings:getSidecarFile("baz.epub"))
end) end)
it("should read legacy history file", function() it("should read legacy history file", function()
@ -65,9 +63,9 @@ describe("docsettings module", function()
} }
for _, f in ipairs(legacy_files) do 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) 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 -- Legacy history files should not be removed before flush has been
-- called. -- called.
assert.Equals(lfs.attributes(f, "mode"), "file") assert.Equals(lfs.attributes(f, "mode"), "file")
@ -80,7 +78,7 @@ describe("docsettings module", function()
assert.True(os.remove(f) == nil) assert.True(os.remove(f) == nil)
end end
assert.False(os.remove(d.doc_sidecar_file) == nil) assert.False(os.remove(d.doc_sidecar_dir.."/"..d.sidecar_filename) == nil)
d:purge() d:purge()
end) end)
@ -98,7 +96,7 @@ describe("docsettings module", function()
for i, v in ipairs(legacy_files) do for i, v in ipairs(legacy_files) do
d:saveSetting("a", i) d:saveSetting("a", i)
d:flush() 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 end
d:close() d:close()
@ -127,33 +125,33 @@ describe("docsettings module", function()
d:saveSetting("a", "a") d:saveSetting("a", "a")
d:flush() d:flush()
-- metadata.pdf.lua should be generated. -- 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() d:flush()
-- metadata.pdf.lua.old should not yet be generated. -- 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 -- make metadata.pdf.lua older to bypass 60s age needed for .old rotation
local minutes_ago = os.time() - 120 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() d:close()
-- metadata.pdf.lua and metadata.pdf.lua.old should be generated. -- 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_dir.."/"..d.sidecar_filename, "mode"))
assert.Equals("file", lfs.attributes(d.doc_sidecar_file .. ".old", "mode")) assert.Equals("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename .. ".old", "mode"))
-- write some garbage to sidecar-file. -- 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:write("bla bla bla")
f_out:close() f_out:close()
d = docsettings:open(file) d = docsettings:open(file)
-- metadata.pdf.lua should be removed. -- metadata.pdf.lua should be removed.
assert.are.not_equal("file", lfs.attributes(d.doc_sidecar_file, "mode")) assert.are.not_equal("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename, "mode"))
assert.Equals("file", lfs.attributes(d.doc_sidecar_file .. ".old", "mode")) assert.Equals("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename .. ".old", "mode"))
assert.Equals("a", d:readSetting("a")) assert.Equals("a", d:readSetting("a"))
d:saveSetting("a", "b") d:saveSetting("a", "b")
d:close() d:close()
-- metadata.pdf.lua should be generated. -- 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"))
assert.Equals("file", lfs.attributes(d.doc_sidecar_file .. ".old", "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. -- The contents in sidecar_file and sidecar_file.old are different.
-- a:b v.s. a:a -- a:b v.s. a:a
@ -161,21 +159,21 @@ describe("docsettings module", function()
-- The content should come from sidecar_file. -- The content should come from sidecar_file.
assert.Equals("b", d:readSetting("a")) assert.Equals("b", d:readSetting("a"))
-- write some garbage to sidecar-file. -- 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:write("bla bla bla")
f_out:close() f_out:close()
-- do not flush the result, open docsettings again. -- do not flush the result, open docsettings again.
d = docsettings:open(file) d = docsettings:open(file)
-- metadata.pdf.lua should be removed. -- metadata.pdf.lua should be removed.
assert.are.not_equal("file", lfs.attributes(d.doc_sidecar_file, "mode")) assert.are.not_equal("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename, "mode"))
assert.Equals("file", lfs.attributes(d.doc_sidecar_file .. ".old", "mode")) assert.Equals("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename .. ".old", "mode"))
-- The content should come from sidecar_file.old. -- The content should come from sidecar_file.old.
assert.Equals("a", d:readSetting("a")) assert.Equals("a", d:readSetting("a"))
d:close() d:close()
-- metadata.pdf.lua should be generated. -- 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"))
assert.Equals("file", lfs.attributes(d.doc_sidecar_file .. ".old", "mode")) assert.Equals("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename .. ".old", "mode"))
end) end)
describe("ignore empty sidecar file", function() describe("ignore empty sidecar file", function()
@ -186,29 +184,29 @@ describe("docsettings module", function()
d:saveSetting("a", "a") d:saveSetting("a", "a")
d:flush() d:flush()
-- metadata.pdf.lua should be generated. -- 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 -- make metadata.pdf.lua older to bypass 60s age needed for .old rotation
local minutes_ago = os.time() - 120 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() d:close()
-- metadata.pdf.lua and metadata.pdf.lua.old should be generated. -- 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_dir.."/"..d.sidecar_filename, "mode"))
assert.Equals("file", lfs.attributes(d.doc_sidecar_file .. ".old", "mode")) assert.Equals("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename .. ".old", "mode"))
-- reset the sidecar_file to an empty file. -- 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() f_out:close()
d = docsettings:open(file) d = docsettings:open(file)
-- metadata.pdf.lua should be removed. -- metadata.pdf.lua should be removed.
assert.are.not_equal("file", lfs.attributes(d.doc_sidecar_file, "mode")) assert.are.not_equal("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename, "mode"))
assert.Equals("file", lfs.attributes(d.doc_sidecar_file .. ".old", "mode")) assert.Equals("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename .. ".old", "mode"))
assert.Equals("a", d:readSetting("a")) assert.Equals("a", d:readSetting("a"))
d:saveSetting("a", "b") d:saveSetting("a", "b")
d:close() d:close()
-- metadata.pdf.lua should be generated. -- 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"))
assert.Equals("file", lfs.attributes(d.doc_sidecar_file .. ".old", "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. -- The contents in sidecar_file and sidecar_file.old are different.
-- a:b v.s. a:a -- a:b v.s. a:a
end) end)
@ -220,30 +218,30 @@ describe("docsettings module", function()
d:saveSetting("a", "a") d:saveSetting("a", "a")
d:flush() d:flush()
-- metadata.pdf.lua should be generated. -- 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 -- make metadata.pdf.lua older to bypass 60s age needed for .old rotation
local minutes_ago = os.time() - 120 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() d:close()
-- metadata.pdf.lua and metadata.pdf.lua.old should be generated. -- 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_dir.."/"..d.sidecar_filename, "mode"))
assert.Equals("file", lfs.attributes(d.doc_sidecar_file .. ".old", "mode")) assert.Equals("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename .. ".old", "mode"))
-- reset the sidecar_file to an empty file. -- 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:write("{ } ")
f_out:close() f_out:close()
d = docsettings:open(file) d = docsettings:open(file)
-- metadata.pdf.lua should be removed. -- metadata.pdf.lua should be removed.
assert.are.not_equal("file", lfs.attributes(d.doc_sidecar_file, "mode")) assert.are.not_equal("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename, "mode"))
assert.Equals("file", lfs.attributes(d.doc_sidecar_file .. ".old", "mode")) assert.Equals("file", lfs.attributes(d.doc_sidecar_dir.."/"..d.sidecar_filename .. ".old", "mode"))
assert.Equals("a", d:readSetting("a")) assert.Equals("a", d:readSetting("a"))
d:saveSetting("a", "b") d:saveSetting("a", "b")
d:close() d:close()
-- metadata.pdf.lua should be generated. -- 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"))
assert.Equals("file", lfs.attributes(d.doc_sidecar_file .. ".old", "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. -- The contents in sidecar_file and sidecar_file.old are different.
-- a:b v.s. a:a -- a:b v.s. a:a
end) end)

@ -47,7 +47,7 @@ describe("FileManager module", function()
local tmp_sidecar = docsettings:getSidecarDir(util.realpath(tmp_fn)) local tmp_sidecar = docsettings:getSidecarDir(util.realpath(tmp_fn))
lfs.mkdir(tmp_sidecar) 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 tmp_sidecar_file_foo = tmp_sidecar_file .. ".foo" -- non-docsettings file
local tmpsf = io.open(tmp_sidecar_file, "w") local tmpsf = io.open(tmp_sidecar_file, "w")
tmpsf:write("{}") tmpsf:write("{}")
@ -85,7 +85,7 @@ describe("FileManager module", function()
local tmp_sidecar = docsettings:getSidecarDir(util.realpath(tmp_fn)) local tmp_sidecar = docsettings:getSidecarDir(util.realpath(tmp_fn))
lfs.mkdir(tmp_sidecar) 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") local tmpsf = io.open(tmp_sidecar_file, "w")
tmpsf:write("{}") tmpsf:write("{}")
tmpsf:close() tmpsf:close()

Loading…
Cancel
Save