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.
- checksums.yaml +4 -4
- data/README.md +34 -11
- data/lib/tmsu_file_db.rb +120 -47
- data/lib/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d650f6e958d2689681efefb8e199dce96853ac12
|
4
|
+
data.tar.gz: e7ba48721c169bae0ee59e269b0863972d22bfdb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
139
|
-
|
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
|
-
"#{
|
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
|
-
|
33
|
+
`mkdir -p #{path}`
|
22
34
|
end
|
23
35
|
end
|
24
36
|
|
25
|
-
|
26
|
-
|
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
|
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.
|
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
|
-
|
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 =
|
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
|
-
|
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
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
-
|
111
|
-
if
|
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
|
-
|
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
|
-
|
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
|
-
|
264
|
+
system "tmsu init"
|
206
265
|
puts "making vfs_path #{vfs_path}"
|
207
|
-
|
266
|
+
system "mkdir -p #{vfs_path}"
|
208
267
|
puts "mounting vfs path"
|
209
|
-
|
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
|
-
|
222
|
-
|
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 =
|
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}`
|
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}`
|
242
|
-
system
|
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
|
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
|
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