folder_stash 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8d57c9ab806041af992961e83a9472a27836b63f0d46b42adfc9e2e04b35d860
4
+ data.tar.gz: 10d1be204348138ab0027ffedd519d0ab3cdd56a0733732ddb6d9f4bfef590fc
5
+ SHA512:
6
+ metadata.gz: 3f2810d1dd64f6e67dd7e5afa513bc0b71d2b00dfd306f028e2ad1ac598aea49b2906fdc0c9b342cc3e9a6fe3ccd87f85c9a87354f2eee850d95fb33f0a5e6fe
7
+ data.tar.gz: a9cb901842fc1052f09e620961cc95cee8314c9f2e7f1984bb9282698859cc49352f06769f1d8596ad5f19604ecfe6f45d8452443aa2f1dae8556edc068f66d2
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 Martin Stein
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,66 @@
1
+ = folder_stash
2
+
3
+ The <tt>folder_stash</tt> gem will store files in a directory with a user
4
+ definable number of nested subdirectories in a given path and a maximum number
5
+ of items allowed per subdirectory.
6
+
7
+ New nested subdirectories will be created on demand as a given subdirectory
8
+ reaches the specified limit of items. All created subdirectories will have
9
+ randomized base names.
10
+
11
+ <tt>folder_stash</tt> uses a symlink (<tt>.current_store_path</tt>) to the
12
+ currently available directory. By default the symlink will be in the top level
13
+ storage directory, but it can optionally be placed in any directory.
14
+
15
+ == Installation
16
+
17
+ gem install folder_stash
18
+
19
+ == Usage
20
+
21
+ The basic usage is to create a new instance of FileUsher with the directory in
22
+ which files are to be stored in; the top level storage directory.
23
+
24
+ require 'folder_stash'
25
+
26
+ # create a new FileUsher instance with defaults (2 levels of subdirectories,
27
+ # 10000 items per subdirectory)
28
+ usher = FolderStash::FileUsher.new('~/storage_dir')
29
+
30
+ FileUsher will try to locate the <tt>.current_store_path</tt> symlink, either in
31
+ the top level directory, or, if any other location for the link passed as the
32
+ <tt>link_location</tt> option passed to the
33
+ initializer[rdoc-ref:FolderStash::FileUsher.new].
34
+
35
+ If the symlink does not exist, it will create a new branch (nested path) with
36
+ the number of nested subdirectories given in the <tt>nesting_levels</tt> option
37
+ passed to the initializer[rdoc-ref:FolderStash::FileUsher.new] and create the
38
+ symlink which will point to the terminal (most deeply nested) subdirectory.
39
+
40
+ storage_directory
41
+ ├── .current_store_path -> ~/storage_dir/a1bd81a073a78025/2d9dfcd7a6c329b4
42
+ └── a1bd81a073a78025
43
+ └── 2d9dfcd7a6c329b4
44
+
45
+ If the symlink exists, FileUsher will use the existing subdirectory hierarchy.
46
+
47
+ Files can be copied or moved to the directory the symlink currently points to
48
+ using the {#copy}[rdoc-ref:FolderStash::FileUsher#copy] and
49
+ {#move}[rdoc-ref:FolderStash::FileUsher#move] methods respectively, which both
50
+ will return the path the file was stored to.
51
+
52
+ usher.copy('~/image1.jpg')
53
+ # => "storage_dir/a1bd81a073a78025/2d9dfcd7a6c329b4/image1.jpg"
54
+
55
+ usher.move('~/image2.jpg')
56
+ # => "storage_dir/a1bd81a073a78025/2d9dfcd7a6c329b4/image2.jpg"
57
+
58
+ The path returned will by default start with the top level storage directory. It
59
+ is possible to return the relative path or absolute path by passing the values
60
+ +:relative+ or +:absolute+ as the +pathtype+ option:
61
+
62
+ usher.copy('~/image3.jpg', :pathtype => :relative)
63
+ # => "path/to/storage_dir/a1bd81a073a78025/2d9dfcd7a6c329b4/image3.jpg"
64
+
65
+ usher.copy('~/image4.jpg', :pathtype => :absolute)
66
+ # => "/path/to/storage_dir/a1bd81a073a78025/2d9dfcd7a6c329b4/image4.jpg"
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FolderStash
4
+ module Errors
5
+ # Error that is raises when attempting to create a branch in a folder that
6
+ # can not be branched (typically the terminal).
7
+ class BranchError < StandardError
8
+ # Directory for the folder where the branch was attempted.
9
+ attr_reader :dir
10
+
11
+ def initialize(msg = nil, dir: nil)
12
+ @dir = dir
13
+ msg ||= "Can not branch in #{dir} because it is a tree terminal."
14
+ super msg
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FolderStash
4
+ module Errors
5
+ # Error that is raised if a directory does not exist.
6
+ class NoDirectoryError < StandardError
7
+ # Path for the directory that does not exist.
8
+ attr_reader :dir
9
+
10
+ def initialize(msg = nil, dir: nil)
11
+ @dir = dir
12
+ msg ||= "The directory #{dir} does not exist or is not a directory"
13
+ super msg
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FolderStash
4
+ module Errors
5
+ # Error that is raised when the number of items in a tree has exceeded the
6
+ # maximum number allowed in a tree.
7
+ class TreeLimitExceededError < StandardError
8
+ # Number of subdirectories in a given path (branch) of the tree.
9
+ attr_reader :subdirs
10
+
11
+ # Number of items allowed in a subdirectory.
12
+ attr_reader :subdir_limit
13
+
14
+ # Total number of items allowed in a tree.
15
+ attr_reader :tree_limit
16
+
17
+ def initialize(msg = nil, tree: nil)
18
+ @subdirs = tree.path_length
19
+ @subdir_limit = tree.folder_limit
20
+ @tree_limit = tree.tree_limit
21
+ msg ||= 'The storage tree has reached the limit of allowed items:'\
22
+ " #{subdir_limit} items in #{subdirs} subdirectories"\
23
+ " (#{tree_limit} allowd items in total)."
24
+ super msg
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'errors/branch_error'
4
+ require_relative 'errors/no_directory_error'
5
+ require_relative 'errors/tree_limit_exceeded_error'
6
+
7
+ module FolderStash
8
+ # Contains error classes.
9
+ module Errors
10
+ end
11
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FolderStash
4
+ # FileUsher stores files in a directory tree with a defined maximum number of
5
+ # #items per directory.
6
+ #
7
+ # It will create new subdirectories to store files in if a subdirectory has
8
+ # reached the maximum number of items.
9
+ class FileUsher
10
+ CURRENT_STORE_PATH = '.current_store_path'
11
+
12
+ # The working directory where all subdirectories and files are stored.
13
+ attr_reader :directory
14
+
15
+ # An instance of FolderTree.
16
+ attr_reader :tree
17
+
18
+ # Returns a new instance.
19
+ #
20
+ # ===== Arguments
21
+ #
22
+ # * +dir+ (_String_) - path for the #directory.
23
+ #
24
+ # ===== Options
25
+ #
26
+ # * <tt>nesting_levels</tt> - the number of subdirectories below #directory
27
+ # in the path of files that are stored (_default_: +2+).
28
+ # * <tt>folder_limit</tt> - the maximum number of items allowed per
29
+ # directory (_default_: +10000+).
30
+ # * <tt>link_location</tt> - the directory where the #current_directory
31
+ # symlink is stored. When not specified, the symlink will be in #directory
32
+ #
33
+ # Setting <tt>nesting_levels</tt> to +nil+ will also set the
34
+ # <tt>folder_limit</tt>. Conversely, setting <tt>folder_limit</tt> to +nil+
35
+ # also set <tt>nesting_levels</tt> to +nil+.
36
+ def initialize(dir, **opts)
37
+ raise Errors::NoDirectoryError, dir: dir unless File.directory? dir
38
+
39
+ @options = { nesting_levels: 2, folder_limit: 10_000 }.update(opts)
40
+ @directory = dir
41
+ @current_directory = File.join @options.fetch(:link_location, directory),
42
+ CURRENT_STORE_PATH
43
+
44
+ @tree = init_existing || init_new(nesting_levels)
45
+ link_target
46
+ end
47
+
48
+ # Copies +file+ to linked path.
49
+ def copy(file, pathtype: :tree)
50
+ path = store_path(file)
51
+ File.open(path, 'wb') { |f| f.write(File.new(file).read) }
52
+ file_path path, pathtype
53
+ end
54
+
55
+ # Returns the full path (_String_) to the current directory symlink.
56
+ def current_directory
57
+ File.expand_path @current_directory
58
+ end
59
+
60
+ # Returns the full directory path for #current_folder
61
+ def current_path
62
+ current_folder.path
63
+ end
64
+
65
+ # The number of items allowed in any directory in a nested directory path.
66
+ #
67
+ # Will be +nil+ if #nesting_levels is +nil+.
68
+ def folder_limit
69
+ return unless @options.fetch :nesting_levels
70
+
71
+ @options.fetch :folder_limit
72
+ end
73
+
74
+ # Returns the directory path the #current_directory symlink points to.
75
+ def linked_path
76
+ File.readlink current_directory
77
+ end
78
+
79
+ # Moves +file+ to the #linked_path.
80
+ def move(file, pathtype: :tree)
81
+ path = store_path(file)
82
+ FileUtils.mv File.expand_path(file), store_path(file)
83
+ file_path path, pathtype
84
+ end
85
+
86
+ # The number of nested subdirectories.
87
+ def nesting_levels
88
+ return unless @options.fetch :folder_limit
89
+
90
+ tree&.path_length || @options.fetch(:nesting_levels)
91
+ end
92
+
93
+ private
94
+
95
+ # Returns the next available folder in #tree. Will raise OutOfStorageError
96
+ # if none is available.
97
+ def available_folder
98
+ folder = tree.available_folder
99
+ raise Errors::TreeLimitExceededError, tree: tree unless folder
100
+
101
+ folder
102
+ end
103
+
104
+ # Returns +true+ if the current directory symlink exists in #directory.
105
+ def current_directory?
106
+ File.exist? current_directory
107
+ end
108
+
109
+ # Returns a Folder that is currently the FolderTree#terminal.
110
+ def current_folder
111
+ tree.terminal
112
+ end
113
+
114
+ # Returns the path for +path+ as +:absolute+, +:relative+, or only the
115
+ # nested subdirectories in the +:tree+.
116
+ def file_path(path, pathtype)
117
+ treepath = tree.branch_path.append(File.basename(path))
118
+ case pathtype
119
+ when :absolute
120
+ File.realpath path
121
+ when :relative
122
+ treepath[0] = directory
123
+ treepath.join('/')
124
+ when :tree
125
+ treepath.join('/')
126
+ end
127
+ end
128
+
129
+ # Creates the folder #tree in an existing directory with #current_directory
130
+ # symlink.
131
+ def init_existing
132
+ return unless current_directory?
133
+
134
+ FolderTree.for_path linked_path, root: directory, limit: folder_limit
135
+ end
136
+
137
+ # Creates the folder #tree for a new direcrory without #current_directory
138
+ # symlink.
139
+ def init_new(levels)
140
+ FolderTree.empty directory, levels: levels, limit: folder_limit
141
+ end
142
+
143
+ # Creates the current_directory symlink, pointing to the #current_path.
144
+ def link_target
145
+ return if current_directory? && linked_path == current_path
146
+
147
+ FileUtils.ln_s File.expand_path(current_path), current_directory
148
+ end
149
+
150
+ # Returns the next available path (_String_) for a file to be stored under.
151
+ def store_path(file)
152
+ update_link if folder_limit && current_folder.count >= folder_limit
153
+ File.join current_directory, File.basename(file)
154
+ end
155
+
156
+ # Creates new subdirectories points the #current_directory symlink to the
157
+ # new #current_folder.
158
+ def update_link
159
+ tree.new_branch_in available_folder
160
+ FileUtils.rm current_directory
161
+ link_target
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FolderStash
4
+ # A Folder represents a directory in the filesystem.
5
+ class Folder
6
+ # Basename for the folder.
7
+ attr_reader :basename
8
+
9
+ # Absolute path for the folder.
10
+ attr_reader :path
11
+
12
+ # Returns a new instance.
13
+ #
14
+ # ===== Arguments
15
+ #
16
+ # * +path+ (String) - path to the directory for the folder.
17
+ def initialize(path)
18
+ @path = File.expand_path path
19
+ @basename = File.basename path
20
+ end
21
+
22
+ def self.folders_for_path_segment(root, segment)
23
+ root_folder = Folder.new root
24
+ segment.inject([root_folder]) do |dirs, dir|
25
+ path = File.join dirs.last.path, dir
26
+ dirs << Folder.new(path)
27
+ end
28
+ end
29
+
30
+ # Returns the number of visible files in the folder.
31
+ def count
32
+ entries.count
33
+ end
34
+
35
+ # Creates the directory path in the immediate parent.
36
+ def create
37
+ FileUtils.mkdir path unless exist?
38
+ end
39
+
40
+ # Creates the directory #path with all parents.
41
+ def create!
42
+ FileUtils.mkdir_p path unless exist?
43
+ end
44
+
45
+ # Returns +true+ if the directory #path exists.
46
+ def exist?
47
+ File.exist? path
48
+ end
49
+
50
+ # Returns a list of entries (files or folders) in the folder.
51
+ #
52
+ # ===== Options
53
+ #
54
+ # * <tt>include_hidden</tt>
55
+ # * +true+ - list visible and hidden entries.
56
+ # * +false+ (_default_) - list only visible entries.
57
+ def entries(include_hidden: false)
58
+ children = Dir.children path
59
+ return children if include_hidden == true
60
+
61
+ children.reject { |entry| entry.start_with? '.' }
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FolderStash
4
+ # A FolderTree represents a nested directory path.
5
+ class FolderTree
6
+ # An array with instances of Folder, one for each directory in a nested
7
+ # directory path from the #root to the #terminal.
8
+ attr_accessor :folders
9
+
10
+ # The maximum number of itmes that may be stored in any folder in #folders.
11
+ attr_reader :folder_limit
12
+
13
+ # The number of items (directories) in a nested directory path, from the
14
+ # #root to the #terminal.
15
+ attr_reader :path_length
16
+
17
+ attr_reader :tree_limit
18
+
19
+ # Returns a new instance.
20
+ #
21
+ # ===== Arguments
22
+ #
23
+ # * +folders+ (String) - Array of Folder instances.
24
+ # * +levels+ (Integer) - Number of nested subdirectories in a path.
25
+ # * +limit+ (Integer) - Number of items allowed in any folder in the
26
+ # tree's directory path.
27
+ def initialize(folders, levels, limit)
28
+ @folders = folders
29
+ @path_length = levels
30
+ @folder_limit = limit
31
+ @tree_limit = folder_limit ? folder_limit**(path_length + 1) : nil
32
+ end
33
+
34
+ def self.empty(root, levels:, limit:)
35
+ folders = [Folder.new(root)]
36
+ tree = new(folders, levels, limit)
37
+ tree.new_branch_in tree.root, levels
38
+ tree
39
+ end
40
+
41
+ def self.for_path(path, root:, limit:)
42
+ path_items = path_segment path, root
43
+ folders = Folder.folders_for_path_segment root, path_items
44
+ new folders, path_items.count, limit
45
+ end
46
+
47
+ def self.path_segment(terminal, root)
48
+ File.expand_path(terminal).split('/') - File.expand_path(root).split('/')
49
+ end
50
+
51
+ # Returns the number of folder in the nested path currently available.
52
+ def actual_path_length
53
+ folders.count
54
+ end
55
+
56
+ # Returns the next available folder, searching upstream from the terminal
57
+ # folder to the #root.
58
+ #
59
+ # Returns #root if root is the only folder.
60
+ def available_folder
61
+ return root if flat?
62
+
63
+ folders.reverse.find { |folder| folder.count < folder_limit }
64
+ end
65
+
66
+ def branch_path
67
+ folders.map(&:basename)
68
+ end
69
+
70
+ def flat?
71
+ actual_path_length == 1 && path_length.nil?
72
+ end
73
+
74
+ # Returns the number (integer) of levels of folders nested in +folder+.
75
+ def levels_below(folder)
76
+ return if flat?
77
+
78
+ path_length - folders.index(folder)
79
+ end
80
+
81
+ # Creates a new branch of folders in +folder+ and updates #folders to the
82
+ # new branch.
83
+ #
84
+ # Returns an array with the full path for the terminal folder in the branch
85
+ # created.
86
+ def new_branch_in(folder, levels = nil)
87
+ return if flat?
88
+
89
+ raise Errors::BranchError, dir: folder.path if folder == terminal
90
+
91
+ raise TreeLimitExceededError, tree: self if folder.count >= folder_limit
92
+
93
+ levels ||= levels_below folder
94
+ new_branch = new_paths_in folder, levels
95
+ @folders = folders[0..folders.index(folder)].concat new_branch
96
+ folders.last.create!
97
+ end
98
+
99
+ # Returns the root folder.
100
+ def root
101
+ folders.first
102
+ end
103
+
104
+ # Returns the terminal (most deeply nested) folder.
105
+ #
106
+ # Returns +nil+ if the tree has not been fully initialized with a branch.
107
+ def terminal
108
+ return root if flat?
109
+
110
+ return if actual_path_length < path_length
111
+
112
+ folders.last
113
+ end
114
+
115
+ private
116
+
117
+ # If the file <tt>path/name</tt> exists, randomize the name until a new
118
+ # random is found that does not exist.
119
+ #
120
+ # Returns the new unique path name.
121
+ #
122
+ # This only needs to be called for the first new directory to be created,
123
+ # all others will be created in empty directories and therefroe always be
124
+ # unique.
125
+ def ensure_unique_node(path, name)
126
+ name = SecureRandom.hex(8) while File.exist? File.join(path, name)
127
+ Folder.new File.join(path, name)
128
+ end
129
+
130
+ # Returns an array of new Folder instances.
131
+ def new_paths_in(folder, count)
132
+ first_node = ensure_unique_node(folder.path, SecureRandom.hex(8))
133
+ remainder = count - 1
134
+ remainder.times.inject([first_node]) do |nodes|
135
+ path = File.join nodes.last.path, SecureRandom.hex(8)
136
+ nodes << Folder.new(path)
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'securerandom'
5
+
6
+ require_relative 'folder_stash/errors'
7
+ require_relative 'folder_stash/file_usher'
8
+ require_relative 'folder_stash/folder'
9
+ require_relative 'folder_stash/folder_tree'
10
+
11
+ # Module that contains the FileUsher, FolderTree, and FolderTree classes.
12
+ module FolderStash
13
+ end
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: folder_stash
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Martin Stein
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-08-15 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |
14
+ The <tt>folder_stash</tt> gem will store files in a directory with a user
15
+ definable number of nested subdirectories in a given path and a maximum
16
+ number of items allowed per subdirectory.
17
+
18
+ New nested subdirectories will be created on demand as a given subdirectory
19
+ reaches the specified limit of items. All created subdirectories will have
20
+ randomized base names.
21
+ email: loveablelobster@fastmail.fm
22
+ executables: []
23
+ extensions: []
24
+ extra_rdoc_files: []
25
+ files:
26
+ - LICENSE
27
+ - README.rdoc
28
+ - lib/folder_stash.rb
29
+ - lib/folder_stash/errors.rb
30
+ - lib/folder_stash/errors/branch_error.rb
31
+ - lib/folder_stash/errors/no_directory_error.rb
32
+ - lib/folder_stash/errors/tree_limit_exceeded_error.rb
33
+ - lib/folder_stash/file_usher.rb
34
+ - lib/folder_stash/folder.rb
35
+ - lib/folder_stash/folder_tree.rb
36
+ homepage: https://github.com/loveablelobster/folder_stash
37
+ licenses:
38
+ - MIT
39
+ metadata: {}
40
+ post_install_message:
41
+ rdoc_options: []
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ required_rubygems_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ requirements: []
55
+ rubygems_version: 3.0.3
56
+ signing_key:
57
+ specification_version: 4
58
+ summary: Keeps the number of files per directory within a limit by autogenerating
59
+ subdirectories.
60
+ test_files: []