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.
- 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