folder_stash 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []