tmsu_file_db 0.0.2 → 0.0.3

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