tmsu_file_db 0.0.2 → 0.0.3

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.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +34 -11
  3. data/lib/tmsu_file_db.rb +120 -47
  4. data/lib/version.rb +1 -1
  5. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 910c386ec16296462c430627b29d00ce9c718874
4
- data.tar.gz: 60c51e32d1ddd33291fe0cbc930b09dd18c6f706
3
+ metadata.gz: d650f6e958d2689681efefb8e199dce96853ac12
4
+ data.tar.gz: e7ba48721c169bae0ee59e269b0863972d22bfdb
5
5
  SHA512:
6
- metadata.gz: a9f561e9efc1c6180264896d66d1bab28d8c8a5ac5b5b16c1f8ed1dd08c668e19f1b596e418ed7a0b288ebda99dfac566ff75066b834b7d50acaec78147f6b41
7
- data.tar.gz: e81b583feb2a1bdadf8c433d38193c9f5965078a080c34e0a4c919c16e51e9e60e0bb12d016f200dc329d4aacd193840c9627e255c8d0518e4feeb83b1fc7683
6
+ metadata.gz: c338e64470b4093743bc8f7b92e47698aa315560ac7904dd30014f8cd23dc6c6d970853a4c32b28788148d2e43850d7cabfd561bf03f335b78e11e09ed2b42c9
7
+ data.tar.gz: a0d1574e6738232cbb320db2e73fa63dc7941751caa276e8e080e925dd04f716a5af196bd9c121cb64e41d818053e0a2e4c432a0c4d6057ddd5d4a8f195e9cc2
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  This is an ORM similar to ActiveRecord, but uses the filesystem instead.
2
2
 
3
- It uses TMSU which is a "non-hierarchical" filesystem tagging system.
3
+ It uses TMSU which is a filesystem tagging system.
4
4
 
5
5
  Usage:
6
6
 
@@ -17,7 +17,8 @@ require 'tmsu_file_db'
17
17
 
18
18
  class User < TmsuModel
19
19
 
20
- # this configure block is optional, it defaults to the current directory
20
+ # this configure block is optional
21
+ # it defaults to a randomly named dir in ./db
21
22
  configure root_path: "./db/users"
22
23
 
23
24
  # Validations must return an array
@@ -35,22 +36,23 @@ end
35
36
 
36
37
  **Create instances**
37
38
 
39
+ ```rb
38
40
  # create, update, and delete
39
41
  u = User.new name: "max"
40
42
  u.valid? # => false
41
43
  u.errors # => ["email isn't valid"]
42
44
  u.save # => false
43
- u.email = "maxpleaner@gmail.com"
45
+ u.[:email] = "maxpleaner@gmail.com"
44
46
  u.valid? # => true
45
47
  u.save # => true
46
48
  u.update(email: "max.pleaner@gmail.com") # => true
47
49
  u.update(email: "") # => false
48
50
 
49
- # There are getter/setter methods for convenience
51
+ # There are getter methods for convenience
52
+ # Setters need to use []=
50
53
  u.name # => "max"
51
54
  u["name"] # => "max"
52
55
  u[:name] # => "max"
53
- u.name = "max p."
54
56
  u[:name] # => "max p."
55
57
 
56
58
  # All these getter/setters are working on 'attributes' under the hood.
@@ -66,12 +68,18 @@ u.write "hello"
66
68
  # The content of the file is not part of the "attributes" i.e. name and email
67
69
  # Those are stored using TMSU tags
68
70
  u.tags # => { email: "max.pleaner@gmail.com", name: "max" }
71
+ u.tags == u.attributes # true
69
72
 
70
73
  # Attributes can be deleted
71
74
  u.delete :name
72
75
  u.tags # => { email: "max.pleaner@gmail.com" }
76
+
77
+ # Records can be deleted (this will destroy the file)
78
+ u.destroy
79
+
73
80
  ```
74
81
 
82
+
75
83
  **Use class-level query methods**
76
84
 
77
85
  _Note that this does not use Arel or any of that jazz. So chaining queries or using joins will not work._
@@ -82,16 +90,24 @@ _Note also that there is no `id` on models, only `path`, which is an absolute pa
82
90
  User.where(name: "max p.")[0].name == "max p." # => true
83
91
  User.find_by(name: "max p.").name == "max p." # => true
84
92
  User.update_all(name: "max") # => true
85
- User.all[0].name == "max" # => true
86
93
 
87
94
  # You can make arbitrary queries using TMSU syntax
88
95
  # e.g. select all users with email set that are not named melvin
89
96
  User.query("name != 'melvin' and email")[0].name == "max" # => true
90
97
  ```
91
98
 
92
- You can also skip `TmsuModel` and use `TmsuRuby.file` instead. This does _not_ handle creation / deletion of files. It should only be used with files that already exist.
99
+ Although there's an index method (`all`), there's no typical auto-incrementing ids stored in TMSU. So to load a single, arbitrary record without tags, its file path is used:
93
100
 
94
- Note that these methods are technically available on `TmsuModel` instances, callable on the `tmsu_file` attribute. But this shoudln't be done, because it will cause the in-memory attributes to be out of sync.
101
+ ```rb
102
+ u.path # => "./db/users/23uj8d9j328dj"
103
+ User.from_file(u.path).name == "max p."
104
+ ```
105
+
106
+ **Use TmsuRuby.file**
107
+
108
+ An alternative to `TmsuModel` is to use `TmsuRuby.file` instead. This does _not_ handle creation / deletion of files. It should only be used with files that already exist.
109
+
110
+ Note that these methods are technically available on `TmsuModel` instances as well. But this shouldn't be done, because it will cause the in-memory attributes to be out of sync. Also, some operations like `tag` will error if called on unsaved records.
95
111
 
96
112
  ```rb
97
113
  file_path = './my_pic.jpg' # this should already exist
@@ -132,11 +148,18 @@ TmsuRuby.file("./my_pic.jpg").tags
132
148
  # => { foo: nil, a: nil, b: nil, d: 2 }
133
149
  ```
134
150
 
135
- Using `TmsuRuby.file` you can search by tag as well:
151
+ Using `TmsuRuby.file` you can search by tag as well. All these methods return
152
+ an array of absolute paths
136
153
 
137
154
  ```rb
138
- # Returns array of paths (files with the tag, systemwide)
139
- TmsuRuby.file("name")
155
+ query_glob = "./**/*.jpg"
156
+
157
+ # To perform a scoped search (the same used by .where, .find_by, and .query):
158
+ # This is a simple query, but the whole TMSU syntax is available
159
+ TmsuRuby.file(query_glob).paths_query("foo")
160
+
161
+ # Search the whole filesystem for files with tag
162
+ TmsuRuby.file.files("foo")
140
163
  ```
141
164
 
142
165
 
data/lib/tmsu_file_db.rb CHANGED
@@ -12,18 +12,31 @@ class TmsuModel
12
12
  Validations = Hash.new { |h,k| h[k] = [] }
13
13
  Callbacks = {}
14
14
 
15
+
15
16
  def self.query_glob
16
- "#{Config[:root_path] || "."}/*"
17
+ "#{root_path}/*"
18
+ end
19
+
20
+ def self.root_path
21
+ Config[:root_path] || generate_path(within: "./db")
22
+ end
23
+
24
+ def self.generate_path(within: '.')
25
+ loop do
26
+ path = "#{within}/#{SecureRandom.hex}"
27
+ break path unless File.exists?(path)
28
+ end
17
29
  end
18
30
 
19
31
  def self.configure(root_path:)
20
32
  Config[:root_path] = root_path || "./db".tap do |path|
21
- `mkdir -p #{path}`
33
+ `mkdir -p #{path}`
22
34
  end
23
35
  end
24
36
 
25
- def self.validate(attribute=nil, &blk)
26
- if attribute
37
+
38
+ def self.validate(attribute=:generic, &blk)
39
+ if attribute == :generic
27
40
  Validations[:generic] << blk
28
41
  else
29
42
  Validations[attribute] << blk
@@ -37,12 +50,18 @@ class TmsuModel
37
50
  def self.opts_to_query opts
38
51
  case opts
39
52
  when Array
40
- opts.join " "
53
+ opts.map { |opt| escape_whitespace opt }.join " "
41
54
  when Hash
42
- opts.map { |k,v| "#{k}={v}" }.join(" ")
55
+ opts.map do |k,v|
56
+ "#{escape_whitespace k}=#{escape_whitespace v}"
57
+ end.join(" ")
43
58
  end
44
59
  end
45
60
 
61
+ def self.create(attrs)
62
+ new(attrs).tap(&:save)
63
+ end
64
+
46
65
  def self.find_by opts
47
66
  where(opts).first
48
67
  end
@@ -51,19 +70,32 @@ class TmsuModel
51
70
  query opts_to_query opts
52
71
  end
53
72
 
54
- def self.all
73
+ def self.escape_whitespace(string)
74
+ string.to_s.gsub(/(?<!\\)\s/, '\ ')
75
+ end
76
+
77
+ def self.escape_hash_whitespace(hash)
78
+ hash.reduce({}) do |result, (k,v)|
79
+ result[escape_whitespace(k)] = escape_whitespace v
80
+ result
81
+ end
82
+ end
83
+
84
+ def self.from_file(path)
85
+ new(escape_hash_whitespace TmsuRuby.file(path).tags) { path }
86
+ end
55
87
 
88
+ def self.all
89
+ Dir.glob(query_glob).map &method(:from_file)
56
90
  end
57
91
 
58
92
  def self.query string
59
- TmsuFile.new(query_glob).paths_query(query).map do |path|
60
- new TmsuFile.new(path).tags
61
- end
93
+ TmsuRuby.file(query_glob).paths_query(string).map &method(:from_file)
62
94
  end
63
95
 
64
96
  def self.update_all opts={}
65
97
  Dir.glob(query_glob).each do |path|
66
- errors = new(path).tap { |inst| inst.update(opts) }.errors
98
+ errors = from_file(path).tap { |inst| inst.update(opts) }.errors
67
99
  unless errors.empty?
68
100
  raise(
69
101
  ArgumentError, "couldn't update all. Path #{path} caused errors: #{errors.join(", ")}"
@@ -74,30 +106,36 @@ class TmsuModel
74
106
  end
75
107
 
76
108
  def self.destroy_all opts={}
77
- Dir.glob(query_glob).each { |path| `rm #{path}` }
78
- true
109
+ Dir.glob(query_glob).tap { |list| list.each { |path| `rm #{path}` } }
79
110
  end
80
111
 
81
112
  attr_reader :attributes, :errors, :path
82
113
 
83
- def initialize(attrs={})
114
+ def initialize(attrs={}, &blk)
84
115
  attrs = attrs.with_indifferent_access
85
- @path = build_id(attrs.delete :id)
116
+ # normally for re-initializing a record from a file, .from_file is used.
117
+ # but there is another way, which is to pass a block to initialize
118
+ # which returns a path string.
119
+ # Example: TmsuModel.new { "file.txt" }.path # => "file.txt"
120
+ @path = blk ? blk.call : build_path
86
121
  @attributes = attrs
87
122
  @persisted = File.exists? @path
88
123
  @errors = []
89
124
  end
90
125
 
91
- def build_id(given=nil)
92
- rand_id = given || SecureRandom.urlsafe_base64
93
- prefix = "#{self.class::Config[:root_path]}"
94
- if prefix
95
- "#{prefix}/#{rand_id}"
96
- else
97
- "#{rand_id}"
126
+ def ensure_root_path
127
+ unless @root_dir_created
128
+ `mkdir -p #{self.class.root_path}`
129
+ @root_dir_created = true
98
130
  end
99
131
  end
100
132
 
133
+ def build_path
134
+ self.class.generate_path(
135
+ within: self.class::Config[:root_path] || "."
136
+ )
137
+ end
138
+
101
139
  def []=(k,v)
102
140
  attributes[k] = v
103
141
  end
@@ -106,11 +144,12 @@ class TmsuModel
106
144
  attributes[k]
107
145
  end
108
146
 
147
+ # Forwards method missing to getter, if possible
148
+ # To respect indifferent access of attributes,
149
+ # uses has_key? instead of keys.include?
109
150
  def method_missing(sym, *arguments, &blk)
110
- attr_name = sym.to_s[0..-1]
111
- if sym.to_s[-1] == "=" && attributes.keys.include?(attr_name)
112
- attributes[attr_name] = arguments[0]
113
- elsif attributes.keys.include? sym
151
+ super unless defined?(attributes) && attributes.is_a?(Hash)
152
+ if attributes.has_key? sym
114
153
  attributes[sym]
115
154
  else
116
155
  super
@@ -156,8 +195,17 @@ class TmsuModel
156
195
 
157
196
  def save
158
197
  ensure_persisted
198
+ ensure_root_path
199
+ original_attributes = tags
200
+ attributes.each do |k,v|
201
+ if !v.nil? && !(original_attributes[k] == v)
202
+ untag "#{k}=#{original_attributes[k]}"
203
+ end
204
+ end
159
205
  return false unless valid?
160
206
  tag attributes
207
+ @persisted = true
208
+ @attributes = tags.with_indifferent_access
161
209
  true
162
210
  end
163
211
 
@@ -175,22 +223,33 @@ class TmsuModel
175
223
  end
176
224
 
177
225
  def destroy
226
+ `tmsu-fs-rm #{path}`
178
227
  `rm #{path}`
228
+ @persisted = false
179
229
  self
180
230
  end
181
231
 
182
232
  def delete(attr)
183
- untag(attr)
233
+ if attributes[attr].nil?
234
+ untag(attr)
235
+ else
236
+ val = attributes[attr]
237
+ untag("#{attr}=#{val}")
238
+ end
184
239
  attributes.delete attr
185
- attr
186
240
  end
187
241
 
188
242
  end
189
243
 
244
+ # This patch doesn't do anything unless ENV["DEBUG"] is set
190
245
  module SystemPatch
191
246
  refine Object do
192
247
  def system string
193
- `#{string}`.tap &method(:puts)
248
+ if ENV["DEBUG"]
249
+ return `#{string}`.tap &method(:puts)
250
+ else
251
+ return `#{string}`
252
+ end
194
253
  end
195
254
  end
196
255
  end
@@ -202,11 +261,11 @@ module TmsuRubyInitializer
202
261
  end
203
262
  def init_tmsu
204
263
  puts "initializing tmsu"
205
- puts system "tmsu init"
264
+ system "tmsu init"
206
265
  puts "making vfs_path #{vfs_path}"
207
- puts system "mkdir -p #{vfs_path}"
266
+ system "mkdir -p #{vfs_path}"
208
267
  puts "mounting vfs path"
209
- puts system "tmsu mount #{vfs_path}"
268
+ system "tmsu mount #{vfs_path}"
210
269
  end
211
270
  def vfs_path
212
271
  "/home/max/tmsu_vfs"
@@ -217,42 +276,60 @@ module TmsuFileAPI
217
276
 
218
277
  using SystemPatch
219
278
 
279
+ def persisted?
280
+ return super if defined?(super)
281
+ File.exists? path
282
+ end
283
+
220
284
  def tags
221
- system("tmsu tags #{path}").split(" ")[1..-1].reduce({}) do |res, tag|
222
- key, val = tag.split("=")
285
+ return {} unless persisted?
286
+ delimiter = /(?<!\\)\s/
287
+ cmd_res = system("tmsu tags #{path}")
288
+ cmd_res.chomp.split(delimiter)[1..-1].reduce({}) do |res, tag|
289
+ key, val = tag.split("=").map do |str|
290
+ str
291
+ end
223
292
  res.tap { res[key] = val }
224
293
  end
225
294
  end
226
295
 
296
+ def require_persisted
297
+ unless persisted?
298
+ raise(RuntimeError, "called tags on unsaved record. path: #{path}")
299
+ end
300
+ end
301
+
227
302
  def paths_query(query)
228
- query_root = "#{vfs_path}/queries/#{query}"
303
+ query_root = %{#{vfs_path}/queries/"#{query}"}
229
304
  system("ls #{query_root}").split("\n").map do |filename|
230
305
  system("readlink #{query_root}/#{filename}").chomp
231
306
  end
232
307
  end
233
308
 
234
309
  def untag tag_list
235
- `touch #{path}` unless persisted?
310
+ `touch #{path}`
236
311
  system "tmsu untag #{path} #{tag_list}"
237
312
  tags
238
313
  end
239
314
 
240
315
  def tag tag_obj
241
- `touch #{path}` unless persisted?
242
- system "tmsu tag #{path} #{build_tag_arg tag_obj}"
316
+ `touch #{path}`
317
+ system %{tmsu tag #{path} #{build_tag_arg tag_obj}}
243
318
  tags
244
319
  end
245
320
 
246
321
  def build_tag_arg obj
247
322
  case obj
248
323
  when String
249
- obj
324
+ %{"#{obj}"}
250
325
  when Array
251
- obj.join " "
326
+ obj.map { |x| %{"#{x}"} }.join " "
252
327
  when Hash
253
- obj.map {|k,v| "#{k}=#{v}" }.join " "
328
+ obj.map do |k,v|
329
+ %{"#{k}=#{v}"}
330
+ end.join " "
254
331
  else
255
- obj
332
+ %{"#{obj}"}
256
333
  end
257
334
  end
258
335
 
@@ -268,7 +345,7 @@ module TmsuFileAPI
268
345
  when Array
269
346
  tag_obj.join(" ")
270
347
  end
271
- system "tmsu tag --tags '#{build_tag_arg tag_obj}' #{path}"
348
+ system "tmsu untag --tags '#{build_tag_arg tag_obj}' #{path}"
272
349
  files tag_obj
273
350
  end
274
351
 
@@ -294,10 +371,6 @@ class TmsuRuby
294
371
  TmsuRuby::TmsuFile.new path, vfs_path
295
372
  end
296
373
 
297
- def self.model(path=nil)
298
- record = TmsuModel.new id: path
299
- end
300
-
301
374
  class TmsuFile
302
375
  include TmsuFileAPI
303
376
  attr_reader :path, :vfs_path
data/lib/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module TmsuFileDb
2
- VERSION = '0.0.2'
2
+ VERSION = '0.0.3'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tmsu_file_db
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - maxpleaner