diff --git a/lib/imap/backup/account.rb b/lib/imap/backup/account.rb index cd57170c..9bc9bfb6 100644 --- a/lib/imap/backup/account.rb +++ b/lib/imap/backup/account.rb @@ -1,7 +1,6 @@ require "json" require "imap/backup/account/client_factory" -require "imap/backup/account/restore" module Imap; end @@ -98,14 +97,6 @@ def capabilities client.capability end - # Restore the local backup to the server - # - # @return [void] - def restore - restore = Account::Restore.new(account: self) - restore.run - end - # Indicates whether the account has been configured, and is ready # to be used # diff --git a/lib/imap/backup/cli/folder_enumerator.rb b/lib/imap/backup/account/folder_mapper.rb similarity index 95% rename from lib/imap/backup/cli/folder_enumerator.rb rename to lib/imap/backup/account/folder_mapper.rb index 824d0c95..43453a5d 100644 --- a/lib/imap/backup/cli/folder_enumerator.rb +++ b/lib/imap/backup/account/folder_mapper.rb @@ -7,22 +7,22 @@ module Imap; end module Imap::Backup - class CLI < Thor; end + class Account; end # Implements a folder enumerator for backed-up accounts - class CLI::FolderEnumerator + class Account::FolderMapper def initialize( + account:, destination:, - source:, destination_delimiter: "/", destination_prefix: "", source_delimiter: "/", source_prefix: "" ) + @account = account @destination = destination @destination_delimiter = destination_delimiter @destination_prefix = destination_prefix - @source = source @source_delimiter = source_delimiter @source_prefix = source_prefix end @@ -48,7 +48,7 @@ def each attr_reader :destination attr_reader :destination_delimiter - attr_reader :source + attr_reader :account attr_reader :source_delimiter def destination_prefix_clipped @@ -94,7 +94,7 @@ def destination_folder_for_path(name) end def source_local_path - source.local_path + account.local_path end def source_folder_name(imap_pathname) diff --git a/lib/imap/backup/account/restore.rb b/lib/imap/backup/account/restore.rb index 3fbe50e6..ffce30b8 100644 --- a/lib/imap/backup/account/restore.rb +++ b/lib/imap/backup/account/restore.rb @@ -1,4 +1,4 @@ -require "imap/backup/account/serialized_folders" +require "imap/backup/account/folder_mapper" require "imap/backup/uploader" module Imap; end @@ -8,15 +8,16 @@ class Account; end # Restores all backed up folders to the server class Account::Restore - def initialize(account:) + def initialize(account:, delimiter: "/", prefix: "") @account = account + @destination_delimiter = delimiter + @destination_prefix = prefix end # Runs the restore operation # @return [void] def run - serialized_folders = Account::SerializedFolders.new(account: account) - serialized_folders.each do |serializer, folder| + folders.each do |serializer, folder| Uploader.new(folder, serializer).run end end @@ -24,5 +25,20 @@ def run private attr_reader :account + attr_reader :destination_delimiter + attr_reader :destination_prefix + + def enumerator_options + { + account: account, + destination: account, + destination_delimiter: destination_delimiter, + destination_prefix: destination_prefix + } + end + + def folders + Account::FolderMapper.new(**enumerator_options) + end end end diff --git a/lib/imap/backup/cli.rb b/lib/imap/backup/cli.rb index 1fee3c14..078ff151 100644 --- a/lib/imap/backup/cli.rb +++ b/lib/imap/backup/cli.rb @@ -229,6 +229,17 @@ def mirror(source_email, destination_email) config_option quiet_option verbose_option + method_option( + "delimiter", + type: :string, + desc: "the delimiter for folder names" + ) + method_option( + "prefix", + type: :string, + desc: "a prefix (namespace) to add to folder names", + aliases: ["-d"] + ) # Restores backed up emails to an account # @return [void] def restore(email = nil) diff --git a/lib/imap/backup/cli/restore.rb b/lib/imap/backup/cli/restore.rb index 64932458..88a88a17 100644 --- a/lib/imap/backup/cli/restore.rb +++ b/lib/imap/backup/cli/restore.rb @@ -1,5 +1,6 @@ require "thor" +require "imap/backup/account/restore" require "imap/backup/cli/helpers" require "imap/backup/logger" @@ -28,18 +29,18 @@ def run case when email && !options.key?(:accounts) account = account(config, email) - account.restore + restore(account, **restore_options) when !email && !options.key?(:accounts) Logger.logger.info "Calling restore without an EMAIL parameter is deprecated" - config.accounts.map(&:restore) + config.accounts.each { |a| restore(a) } when email && options.key?(:accounts) raise "Missing EMAIL parameter" when !email && options.key?(:accounts) Logger.logger.info( - "Calling restore with the --account option is deprected, " \ + "Calling restore with the --account option is deprecated, " \ "please pass a single EMAIL parameter" ) - requested_accounts(config).each(&:restore) + requested_accounts(config).each { |a| restore(a) } end end end @@ -48,5 +49,14 @@ def run attr_reader :email attr_reader :options + + def restore(account, **options) + restore = Account::Restore.new(account: account, **options) + restore.run + end + + def restore_options + options.slice(:delimiter, :prefix) + end end end diff --git a/lib/imap/backup/cli/transfer.rb b/lib/imap/backup/cli/transfer.rb index 93f24b61..b3a03675 100644 --- a/lib/imap/backup/cli/transfer.rb +++ b/lib/imap/backup/cli/transfer.rb @@ -1,6 +1,6 @@ +require "imap/backup/account/folder_mapper" require "imap/backup/cli/backup" require "imap/backup/cli/helpers" -require "imap/backup/cli/folder_enumerator" require "imap/backup/logger" require "imap/backup/migrator" require "imap/backup/mirror" @@ -9,15 +9,13 @@ module Imap; end module Imap::Backup # Implements migration and mirroring - class CLI::Transfer < Thor - include Thor::Actions + class CLI::Transfer include CLI::Helpers - # The possible vaues for the action parameter + # The possible values for the action parameter ACTIONS = %i(migrate mirror).freeze def initialize(action, source_email, destination_email, options) - super([]) @action = action @source_email = source_email @destination_email = destination_email @@ -35,22 +33,20 @@ def initialize(action, source_email, destination_email, options) # @raise [RuntimeError] if the indicated action is unknown, # or the source and destination accounts are the same, # or either of the accounts is not configured, - # or incompatible namespace/delimter parameters have been supplied + # or incompatible namespace/delimiter parameters have been supplied # @return [void] - no_commands do - def run - raise "Unknown action '#{action}'" if !ACTIONS.include?(action) - - process_options! - prepare_mirror if action == :mirror - - folders.each do |serializer, folder| - case action - when :migrate - Migrator.new(serializer, folder, reset: reset).run - when :mirror - Mirror.new(serializer, folder).run - end + def run + raise "Unknown action '#{action}'" if !ACTIONS.include?(action) + + process_options! + prepare_mirror if action == :mirror + + folders.each do |serializer, folder| + case action + when :migrate + Migrator.new(serializer, folder, reset: reset).run + when :mirror + Mirror.new(serializer, folder).run end end end @@ -148,17 +144,17 @@ def config def enumerator_options { + account: source_account, destination: destination_account, destination_delimiter: destination_delimiter, destination_prefix: destination_prefix, - source: source_account, source_delimiter: source_delimiter, source_prefix: source_prefix } end def folders - CLI::FolderEnumerator.new(**enumerator_options) + Account::FolderMapper.new(**enumerator_options) end def destination_account diff --git a/lib/imap/backup/serializer/delayed_metadata_serializer.rb b/lib/imap/backup/serializer/delayed_metadata_serializer.rb index 2976d2b7..c643b0a9 100644 --- a/lib/imap/backup/serializer/delayed_metadata_serializer.rb +++ b/lib/imap/backup/serializer/delayed_metadata_serializer.rb @@ -8,6 +8,8 @@ module Imap; end module Imap::Backup + class Serializer; end + # Wraps the Serializer, delaying metadata appends class Serializer::DelayedMetadataSerializer extend Forwardable diff --git a/lib/imap/backup/serializer/imap.rb b/lib/imap/backup/serializer/imap.rb index fa412c49..bd9ee742 100644 --- a/lib/imap/backup/serializer/imap.rb +++ b/lib/imap/backup/serializer/imap.rb @@ -7,6 +7,8 @@ module Imap; end module Imap::Backup + class Serializer; end + # Stores message metadata class Serializer::Imap # The version number to store in the metadata file diff --git a/lib/imap/backup/serializer/mbox.rb b/lib/imap/backup/serializer/mbox.rb index d81f5c4f..9048f7be 100644 --- a/lib/imap/backup/serializer/mbox.rb +++ b/lib/imap/backup/serializer/mbox.rb @@ -3,6 +3,8 @@ module Imap; end module Imap::Backup + class Serializer; end + # Stores messages class Serializer::Mbox # @return [String] The path of the mailbox file, without the '.mbox' extension diff --git a/lib/imap/backup/serializer/message.rb b/lib/imap/backup/serializer/message.rb index 7f656887..1dd24598 100644 --- a/lib/imap/backup/serializer/message.rb +++ b/lib/imap/backup/serializer/message.rb @@ -5,6 +5,8 @@ module Imap; end module Imap::Backup + class Serializer; end + # Represents a stored message class Serializer::Message # @return [Array[Symbol]] the message's flags diff --git a/lib/imap/backup/serializer/message_enumerator.rb b/lib/imap/backup/serializer/message_enumerator.rb index 223a7d65..b55feac6 100644 --- a/lib/imap/backup/serializer/message_enumerator.rb +++ b/lib/imap/backup/serializer/message_enumerator.rb @@ -1,6 +1,8 @@ module Imap; end module Imap::Backup + class Serializer; end + # Enumerates over a list of stores messages class Serializer::MessageEnumerator # @param imap [Serializer::Imap] the metadata serializer for the folder diff --git a/lib/imap/backup/serializer/permission_checker.rb b/lib/imap/backup/serializer/permission_checker.rb index 483ddbe1..ea658834 100644 --- a/lib/imap/backup/serializer/permission_checker.rb +++ b/lib/imap/backup/serializer/permission_checker.rb @@ -3,6 +3,8 @@ module Imap; end module Imap::Backup + class Serializer; end + # Ensures a file has the desired permissions class Serializer::PermissionChecker # @param filename [String] the file name diff --git a/lib/imap/backup/serializer/transaction.rb b/lib/imap/backup/serializer/transaction.rb index d6302c22..3ac3c0af 100644 --- a/lib/imap/backup/serializer/transaction.rb +++ b/lib/imap/backup/serializer/transaction.rb @@ -1,6 +1,8 @@ module Imap; end module Imap::Backup + class Serializer; end + # Stores data during a transaction class Serializer::Transaction # @return the transaction's stored data diff --git a/lib/imap/backup/serializer/unused_name_finder.rb b/lib/imap/backup/serializer/unused_name_finder.rb index 7c472acb..3c10c1eb 100644 --- a/lib/imap/backup/serializer/unused_name_finder.rb +++ b/lib/imap/backup/serializer/unused_name_finder.rb @@ -3,6 +3,8 @@ module Imap; end module Imap::Backup + class Serializer; end + # Finds a name that can be used to rename a serialized folder class Serializer::UnusedNameFinder # @param serializer [Serializer] a folder serializer diff --git a/lib/imap/backup/serializer/version2_migrator.rb b/lib/imap/backup/serializer/version2_migrator.rb index 6eb0f999..95adcd68 100644 --- a/lib/imap/backup/serializer/version2_migrator.rb +++ b/lib/imap/backup/serializer/version2_migrator.rb @@ -5,6 +5,8 @@ module Imap; end module Imap::Backup + class Serializer; end + # Migrates serialized folder metadata from the version 2 format to the version 3 format class Serializer::Version2Migrator # @param folder_path [String] the base pathv(without extension) of the folder backup diff --git a/spec/features/restore_spec.rb b/spec/features/restore_spec.rb index cf87e7bd..35bff0c9 100644 --- a/spec/features/restore_spec.rb +++ b/spec/features/restore_spec.rb @@ -24,7 +24,8 @@ email: email, folder: folder, flags: [:Draft, :$NON_SYSTEM_FLAG], **message_two ) end - let(:run_command) { run_command_and_stop("imap-backup restore #{email}") } + let(:command) { "imap-backup restore #{email}" } + let(:run_command) { run_command_and_stop(command) } let(:cleanup) do test_server.delete_folder folder test_server.disconnect @@ -195,6 +196,21 @@ end end + context "when a prefix and delimiter are supplied" do + after do + test_server.delete_folder "CIAO.#{folder}" + test_server.delete_folder "CIAO" + end + + let(:command) { "imap-backup restore #{email} --prefix CIAO --delimiter ." } + + it "prepends the prefix to the folder name" do + run_command + + expect(test_server.folders.map(&:name)).to include("CIAO.#{folder}") + end + end + context "when a config path is supplied" do let(:custom_config_path) { File.join(File.expand_path("~/.imap-backup"), "foo.json") } let(:config_options) { super().merge(path: custom_config_path) } @@ -215,11 +231,7 @@ **message_one ) end - let(:run_command) do - run_command_and_stop( - "imap-backup restore #{email} --config #{custom_config_path}" - ) - end + let(:command) { "imap-backup restore #{email} --config #{custom_config_path}" } it "does not raise any errors" do run_command diff --git a/spec/unit/cli/folder_enumerator_spec.rb b/spec/unit/account/folder_mapper_spec.rb similarity index 94% rename from spec/unit/cli/folder_enumerator_spec.rb rename to spec/unit/account/folder_mapper_spec.rb index 7298b491..e77e8fef 100644 --- a/spec/unit/cli/folder_enumerator_spec.rb +++ b/spec/unit/account/folder_mapper_spec.rb @@ -1,15 +1,15 @@ -require "imap/backup/cli/folder_enumerator" +require "imap/backup/account/folder_mapper" require "imap/backup/account" require "imap/backup/client/default" module Imap::Backup - RSpec.describe CLI::FolderEnumerator do + RSpec.describe Account::FolderMapper do subject { described_class.new(**options) } let(:path) { "folder_enumerator_path/foo.imap" } let(:imap_pathname) { Pathname.new(path) } - let(:options) { {source: source, destination: destination} } + let(:options) { {account: source, destination: destination} } let(:source) do instance_double(Account, username: "source", local_path: "folder_enumerator_path") end diff --git a/spec/unit/account/restore_spec.rb b/spec/unit/account/restore_spec.rb index d34a4aed..a431a537 100644 --- a/spec/unit/account/restore_spec.rb +++ b/spec/unit/account/restore_spec.rb @@ -2,15 +2,16 @@ module Imap::Backup RSpec.describe Account::Restore do - subject { described_class.new(account: account) } + subject { described_class.new(account: account, **options) } let(:account) { "account" } - let(:serialized_folders) { instance_double(Account::SerializedFolders) } + let(:options) { {} } + let(:folder_mapper) { instance_double(Account::FolderMapper) } let(:uploader) { instance_double(Uploader, run: nil) } before do - allow(Account::SerializedFolders).to receive(:new) { serialized_folders } - allow(serialized_folders).to receive(:each).and_yield("folder", "serializer") + allow(Account::FolderMapper).to receive(:new) { folder_mapper } + allow(folder_mapper).to receive(:each).and_yield("serializer", "folder") allow(Uploader).to receive(:new) { uploader } end @@ -19,5 +20,31 @@ module Imap::Backup expect(uploader).to have_received(:run) end + + context "when a delimiter is provided" do + let(:options) { {delimiter: "."} } + let(:delimited_folder) { instance_double(Account::Folder) } + let(:serializer) { instance_double(Serializer) } + + it "maps destination folders with the delimiter" do + subject.run + + expect(Account::FolderMapper).to have_received(:new). + with(hash_including(destination_delimiter: ".")) + end + end + + context "when a prefix is provided" do + let(:options) { {prefix: "."} } + let(:delimited_folder) { instance_double(Account::Folder) } + let(:serializer) { instance_double(Serializer) } + + it "maps destination folders with the prefix" do + subject.run + + expect(Account::FolderMapper).to have_received(:new). + with(hash_including(destination_prefix: ".")) + end + end end end diff --git a/spec/unit/account_spec.rb b/spec/unit/account_spec.rb index 028a2d59..c3843235 100644 --- a/spec/unit/account_spec.rb +++ b/spec/unit/account_spec.rb @@ -97,20 +97,6 @@ module Imap::Backup end end - describe "#restore" do - let(:restore) { instance_double(Account::Restore, run: nil) } - - before do - allow(Account::Restore).to receive(:new) { restore } - end - - it "runs restore" do - subject.restore - - expect(restore).to have_received(:run) - end - end - describe "#valid?" do context "with username and password" do it "is true" do diff --git a/spec/unit/cli/restore_spec.rb b/spec/unit/cli/restore_spec.rb index bb61f77f..6ad664f8 100644 --- a/spec/unit/cli/restore_spec.rb +++ b/spec/unit/cli/restore_spec.rb @@ -9,12 +9,14 @@ module Imap::Backup let(:email) { "email" } let(:options) { {} } - let(:account) { instance_double(Account, username: email, restore: nil) } + let(:account) { instance_double(Account, username: email) } let(:config) { instance_double(Configuration, accounts: [account]) } + let(:restore) { instance_double(Account::Restore, run: nil) } before do allow(Configuration).to receive(:exist?) { true } allow(Configuration).to receive(:new) { config } + allow(Account::Restore).to receive(:new) { restore } end it_behaves_like( @@ -22,54 +24,61 @@ module Imap::Backup action: ->(subject) { subject.run } ) - describe "#run" do - context "when an email is provided" do - it "runs restore on the account" do - subject.run + it "runs restore on the account" do + subject.run + + expect(restore).to have_received(:run) + end + + context "when options are provided" do + let(:options) { {delimiter: "/", prefix: "CIAO"} } - expect(account).to have_received(:restore) - end + it "passes them to the restore" do + subject.run + + expect(Account::Restore).to have_received(:new). + with(hash_including(delimiter: "/", prefix: "CIAO")) end + end - context "when neither an email nor a list of account names is provided" do - let(:email) { nil } - let(:options) { {} } + context "when neither an email nor a list of account names is provided" do + let(:email) { nil } + let(:options) { {} } - before do - allow(subject).to receive(:requested_accounts) { [account] } - end + before do + allow(subject).to receive(:requested_accounts) { [account] } + end - it "runs restore on each account" do - subject.run + it "runs restore on each account" do + subject.run - expect(account).to have_received(:restore) - end + expect(restore).to have_received(:run) end + end - context "when an email and a list of account names is provided" do - let(:email) { "email" } - let(:options) { {accounts: "email2"} } + context "when an email and a list of account names is provided" do + let(:email) { "email" } + let(:options) { {accounts: "email2"} } - it "fails" do - expect do - subject.run - end.to raise_error(RuntimeError, /Missing EMAIL parameter/) - end + it "fails" do + expect do + subject.run + end.to raise_error(RuntimeError, /Missing EMAIL parameter/) end + end - context "when just a list of account names is provided" do - let(:email) { nil } - let(:options) { {accounts: "email2"} } + context "when just a list of account names is provided" do + let(:email) { nil } + let(:options) { {accounts: "email2"} } - before do - allow(subject).to receive(:requested_accounts) { [account] } - end + before do + allow(subject).to receive(:requested_accounts) { [account] } + end - it "runs restore on each account" do - subject.run + it "runs restore on each account" do + subject.run - expect(account).to have_received(:restore) - end + expect(restore).to have_received(:run) end end end diff --git a/spec/unit/cli/transfer_spec.rb b/spec/unit/cli/transfer_spec.rb index 35e4d41b..184e9b6b 100644 --- a/spec/unit/cli/transfer_spec.rb +++ b/spec/unit/cli/transfer_spec.rb @@ -35,7 +35,7 @@ module Imap::Backup let(:migrator) { instance_double(Migrator, run: nil) } let(:mirror) { instance_double(Mirror, run: nil) } let(:backup) { instance_double(CLI::Backup, "backup_1", run: nil) } - let(:folder_enumerator) { instance_double(CLI::FolderEnumerator) } + let(:folder_mapper) { instance_double(Account::FolderMapper) } before do allow(Configuration).to receive(:exist?) { true } @@ -43,8 +43,8 @@ module Imap::Backup allow(CLI::Backup).to receive(:new) { backup } allow(Migrator).to receive(:new) { migrator } allow(Mirror).to receive(:new) { mirror } - allow(CLI::FolderEnumerator).to receive(:new) { folder_enumerator } - allow(folder_enumerator).to receive(:each).and_yield(serializer, folder) + allow(Account::FolderMapper).to receive(:new) { folder_mapper } + allow(folder_mapper).to receive(:each).and_yield(serializer, folder) end it_behaves_like( @@ -173,7 +173,7 @@ module Imap::Backup it "uses the values from the servers" do subject.run - expect(CLI::FolderEnumerator).to have_received(:new). + expect(Account::FolderMapper).to have_received(:new). with( hash_including( { @@ -211,7 +211,7 @@ module Imap::Backup it "defaults to '#{default}'" do subject.run - expect(CLI::FolderEnumerator).to have_received(:new). + expect(Account::FolderMapper).to have_received(:new). with(hash_including({parameter => default})) end end @@ -225,7 +225,7 @@ module Imap::Backup it "uses the supplied value" do subject.run - expect(CLI::FolderEnumerator).to have_received(:new). + expect(Account::FolderMapper).to have_received(:new). with(hash_including({parameter => "x"})) end end