rename drivers to stores, make server, render_all store-agnostic
s-ol
3 years ago
13 | 13 | |
14 | 14 | -- usage: |
15 | 15 | -- moon import.moon <root> [output.sqlite3] |
16 | { root, file } = arg | |
16 | { root, store } = arg | |
17 | 17 | |
18 | 18 | assert root, "please specify the root directory" |
19 | 19 |
8 | 8 | |
9 | 9 | require 'mmm' |
10 | 10 | import load_tree from require 'mmm.mmmfs.fileder' |
11 | import get_store from require 'mmm.mmmfs.stores' | |
11 | 12 | import render from require 'mmm.mmmfs.layout' |
12 | import SQLStore from require 'mmm.mmmfs.drivers.sql' | |
13 | 13 | |
14 | 14 | -- usage: |
15 | -- moon render_all.moon [db.sqlite3] [startpath] | |
16 | { file, startpath } = arg | |
15 | -- moon render_all.moon [STORE] [startpath] | |
16 | { store, startpath } = arg | |
17 | 17 | |
18 | 18 | export STATIC |
19 | 19 | STATIC = true |
20 | 20 | |
21 | tree = load_tree SQLStore :file | |
21 | store = get_store store | |
22 | tree = load_tree store | |
22 | 23 | tree = tree\walk startpath if startpath |
23 | 24 | |
24 | 25 | for fileder in coroutine.wrap tree\iterate |
9 | 9 | require 'mmm' |
10 | 10 | |
11 | 11 | import Key, dir_base, load_tree from require 'mmm.mmmfs.fileder' |
12 | import SQLStore from require 'mmm.mmmfs.drivers.sql' | |
12 | import get_store from require 'mmm.mmmfs.stores' | |
13 | 13 | import decodeURI from require 'http.util' |
14 | 14 | |
15 | 15 | lfs = require 'lfs' |
163 | 163 | print msg |
164 | 164 | |
165 | 165 | -- usage: |
166 | -- moon server.moon [db.sqlite3] [host] [port] | |
167 | { file, host, port } = arg | |
166 | -- moon server.moon [STORE] [host] [port] | |
167 | { store, host, port } = arg | |
168 | 168 | |
169 | tree = load_tree SQLStore :file | |
169 | store = get_store store | |
170 | tree = load_tree store | |
170 | 171 | server = Server tree, :host, port: port and tonumber port |
171 | 172 | server\listen! |
0 | export MODE, print, warn, relative, on_client | |
0 | export MODE, print, warn, relative | |
1 | 1 | MODE = 'SERVER' |
2 | 2 | |
3 | 3 | deep_tostring = (tbl, space='') -> |
28 | 28 | _require = require |
29 | 29 | |
30 | 30 | (base, sub) -> |
31 | sub = 0 unless 'number' == type sub | |
31 | sub = sub or 0 | |
32 | 32 | |
33 | 33 | for i=1, sub |
34 | 34 | base = base\match '^(.*)%.%w+$' |
0 | lfs = require 'lfs' | |
1 | ||
2 | -- split filename into dirname + basename | |
3 | dir_base = (path) -> | |
4 | dir, base = path\match '(.-)([^/]-)$' | |
5 | if dir and #dir > 0 | |
6 | dir = dir\sub 1, #dir - 1 | |
7 | ||
8 | dir, base | |
9 | ||
10 | ||
11 | class LFSStore | |
12 | new: (opts = {}) => | |
13 | opts.root or= 'root' | |
14 | opts.verbose or= false | |
15 | ||
16 | if not opts.verbose | |
17 | @log = -> | |
18 | ||
19 | -- ensure path doesnt end with a slash | |
20 | @root = opts.root\match '^(.-)/?$' | |
21 | @log "opening '#{opts.root}'..." | |
22 | ||
23 | log: (...) => | |
24 | print "[DB]", ... | |
25 | ||
26 | -- fileders | |
27 | list_fileders_in: (path='') => | |
28 | coroutine.wrap -> | |
29 | for entry_name in lfs.dir @root .. path | |
30 | continue if '.' == entry_name\sub 1, 1 | |
31 | entry_path = @root .. "#{path}/#{entry_name}" | |
32 | if 'directory' == lfs.attributes entry_path, 'mode' | |
33 | coroutine.yield "#{path}/#{entry_name}" | |
34 | ||
35 | list_all_fileders: (path='') => | |
36 | coroutine.wrap -> | |
37 | for path in @list_fileders_in path | |
38 | coroutine.yield path | |
39 | for p in @list_all_fileders path | |
40 | coroutine.yield p | |
41 | ||
42 | create_fileder: (parent, name) => | |
43 | @log "creating fileder #{path}" | |
44 | path = "#{parent}/#{name}" | |
45 | assert lfs.mkdir @root .. path | |
46 | path | |
47 | ||
48 | remove_fileder: (path) => | |
49 | @log "removing fileder #{path}" | |
50 | ||
51 | rmdir = (path) -> | |
52 | for file in lfs.dir path | |
53 | continue if '.' == file\sub 1, 1 | |
54 | ||
55 | file_path = "#{path}/#{file}" | |
56 | switch lfs.attributes file_path, 'mode' | |
57 | when 'file' | |
58 | assert os.remove file_path | |
59 | when 'directory' | |
60 | assert rmdir file_path | |
61 | ||
62 | lfs.rmdir path | |
63 | ||
64 | rmdir @root .. path | |
65 | ||
66 | rename_fileder: (path, next_name) => | |
67 | @log "renaming fileder #{path} -> '#{next_name}'" | |
68 | parent, name = dir_base path | |
69 | assert os.rename path, @root .. "#{parent}/#{next_name}" | |
70 | ||
71 | move_fileder: (path, next_parent) => | |
72 | @log "moving fileder #{path} -> #{next_parent}/" | |
73 | parent, name = dir_base path | |
74 | assert os.rename @root .. path, @root .. "#{next_parent}/#{name}" | |
75 | ||
76 | -- facets | |
77 | list_facets: (path) => | |
78 | coroutine.wrap -> | |
79 | for entry_name in lfs.dir @root .. path | |
80 | entry_path = "#{@root .. path}/#{entry_name}" | |
81 | if 'file' == lfs.attributes entry_path, 'mode' | |
82 | entry_name = (entry_name\match '(.*)%.%w+') or entry_name | |
83 | entry_name = entry_name\gsub '%$', '/' | |
84 | name, type = entry_name\match '(%w+): *(.+)' | |
85 | if not name | |
86 | name = '' | |
87 | type = entry_name | |
88 | ||
89 | coroutine.yield name, type | |
90 | ||
91 | tofp: (path, name, type) => | |
92 | type = "#{name}: #{type}" if #name > 0 | |
93 | type = type\gsub '%/', '$' | |
94 | @root .. "#{path}/#{type}" | |
95 | ||
96 | locate: (path, name, type) => | |
97 | return unless lfs.attributes @root .. path, 'mode' | |
98 | ||
99 | type = type\gsub '%/', '$' | |
100 | name = "#{name}: " if #name > 0 | |
101 | name = name .. type | |
102 | name = name\gsub '([^%w])', '%%%1' | |
103 | ||
104 | local file_name | |
105 | for entry_name in lfs.dir @root .. path | |
106 | if (entry_name\match "^#{name}$") or entry_name\match "^#{name}%.%w+$" | |
107 | if file_name | |
108 | error "two files match #{name}: #{file_name} and #{entry_name}!" | |
109 | file_name = entry_name | |
110 | ||
111 | ||
112 | file_name and @root .. "#{path}/#{file_name}" | |
113 | ||
114 | load_facet: (path, name, type) => | |
115 | filepath = @locate path, name, type | |
116 | return unless filepath | |
117 | file = assert (io.open filepath, 'rb'), "couldn't open facet file '#{filepath}'" | |
118 | with file\read '*all' | |
119 | file\close! | |
120 | ||
121 | create_facet: (path, name, type, blob) => | |
122 | @log "creating facet #{path} | #{name}: #{type}" | |
123 | assert blob, "cant create facet without value!" | |
124 | ||
125 | filepath = @tofp path, name, type | |
126 | if lfs.attributes filepath, 'mode' | |
127 | error "facet file already exists!" | |
128 | ||
129 | file = assert (io.open filepath, 'wb'), "couldn't open facet file '#{filepath}'" | |
130 | file\write blob | |
131 | file\close! | |
132 | ||
133 | remove_facet: (path, name, type) => | |
134 | @log "removing facet #{path} | #{name}: #{type}" | |
135 | ||
136 | filepath = @locate path, name, type | |
137 | assert filepath, "couldn't locate facet!" | |
138 | assert os.remove filepath | |
139 | ||
140 | rename_facet: (path, name, type, next_name) => | |
141 | @log "renaming facet #{path} | #{name}: #{type} -> #{next_name}" | |
142 | filepath = @locate path, name, type | |
143 | assert filepath, "couldn't locate facet!" | |
144 | assert os.rename filepath, @tofp path, next_name, type | |
145 | ||
146 | update_facet: (path, name, type, blob) => | |
147 | @log "updating facet #{path} | #{name}: #{type}" | |
148 | filepath = @locate path, name, type | |
149 | assert filepath, "couldn't locate facet!" | |
150 | file = assert (io.open filepath, 'wb'), "couldn't open facet file '#{filepath}'" | |
151 | file\write blob | |
152 | file\close! | |
153 | ||
154 | { | |
155 | :LFSStore | |
156 | } |
0 | sqlite = require 'sqlite3' | |
1 | root = os.tmpname! | |
2 | ||
3 | class SQLStore | |
4 | new: (opts = {}) => | |
5 | opts.file or= 'db.sqlite3' | |
6 | opts.verbose or= false | |
7 | opts.memory or= false | |
8 | ||
9 | if not opts.verbose | |
10 | @log = -> | |
11 | ||
12 | if opts.memory | |
13 | @log "opening in-memory DB..." | |
14 | @db = sqlite.open_memory! | |
15 | else | |
16 | @log "opening '#{opts.file}'..." | |
17 | @db = sqlite.open opts.file | |
18 | ||
19 | assert @db\exec [[ | |
20 | PRAGMA foreign_keys = ON; | |
21 | PRAGMA case_sensitive_like = ON; | |
22 | CREATE TABLE IF NOT EXISTS fileder ( | |
23 | id INTEGER NOT NULL PRIMARY KEY, | |
24 | path TEXT NOT NULL UNIQUE, | |
25 | parent TEXT REFERENCES fileder(path) | |
26 | ON DELETE CASCADE | |
27 | ON UPDATE CASCADE | |
28 | ); | |
29 | INSERT OR IGNORE INTO fileder (path, parent) VALUES ("", NULL); | |
30 | ||
31 | CREATE TABLE IF NOT EXISTS facet ( | |
32 | fileder_id INTEGER NOT NULL | |
33 | REFERENCES fileder | |
34 | ON UPDATE CASCADE | |
35 | ON DELETE CASCADE, | |
36 | name TEXT NOT NULL, | |
37 | type TEXT NOT NULL, | |
38 | value BLOB NOT NULL, | |
39 | PRIMARY KEY (fileder_id, name, type) | |
40 | ); | |
41 | CREATE INDEX IF NOT EXISTS facet_fileder_id ON facet(fileder_id); | |
42 | CREATE INDEX IF NOT EXISTS facet_name ON facet(name); | |
43 | ]] | |
44 | ||
45 | log: (...) => | |
46 | print "[DB]", ... | |
47 | ||
48 | close: => | |
49 | @db\close! | |
50 | ||
51 | fetch: (q, ...) => | |
52 | stmt = assert @db\prepare q | |
53 | stmt\bind ... if 0 < select '#', ... | |
54 | stmt\irows! | |
55 | ||
56 | fetch_one: (q, ...) => | |
57 | stmt = assert @db\prepare q | |
58 | stmt\bind ... if 0 < select '#', ... | |
59 | stmt\first_irow! | |
60 | ||
61 | exec: (q, ...) => | |
62 | stmt = assert @db\prepare q | |
63 | stmt\bind ... if 0 < select '#', ... | |
64 | res = assert stmt\exec! | |
65 | ||
66 | -- fileders | |
67 | list_fileders_in: (path='') => | |
68 | coroutine.wrap -> | |
69 | for { path } in @fetch 'SELECT path | |
70 | FROM fileder WHERE parent IS ?', path | |
71 | coroutine.yield path | |
72 | ||
73 | list_all_fileders: (path='') => | |
74 | coroutine.wrap -> | |
75 | for path in @list_fileders_in path | |
76 | coroutine.yield path | |
77 | for p in @list_all_fileders path | |
78 | coroutine.yield p | |
79 | ||
80 | create_fileder: (parent, name) => | |
81 | path = "#{parent}/#{name}" | |
82 | ||
83 | @log "creating fileder #{path}" | |
84 | @exec 'INSERT INTO fileder (path, parent) | |
85 | VALUES (:path, :parent)', | |
86 | { :path, :parent } | |
87 | ||
88 | changes = @fetch_one 'SELECT changes()' | |
89 | assert changes[1] == 1, "couldn't create fileder - parent missing?" | |
90 | path | |
91 | ||
92 | remove_fileder: (path) => | |
93 | @log "removing fileder #{path}" | |
94 | @exec 'DELETE FROM fileder | |
95 | WHERE path LIKE :path || "/%" | |
96 | OR path = :path', path | |
97 | ||
98 | rename_fileder: (path, next_name) => | |
99 | @log "renaming fileder #{path} -> '#{next_name}'" | |
100 | error 'not implemented' | |
101 | ||
102 | @exec 'UPDATE fileder | |
103 | SET path = parent || "/" || :next_name | |
104 | WHERE path = :path', | |
105 | { :path, :next_name } | |
106 | ||
107 | -- @TODO: rename all children, child-children... | |
108 | ||
109 | move_fileder: (path, next_parent) => | |
110 | @log "moving fileder #{path} -> #{next_parent}/" | |
111 | error 'not implemented' | |
112 | ||
113 | -- @TODO: remove all children, child-children... | |
114 | ||
115 | -- facets | |
116 | list_facets: (path) => | |
117 | coroutine.wrap -> | |
118 | for { name, type } in @fetch 'SELECT facet.name, facet.type | |
119 | FROM facet | |
120 | INNER JOIN fileder ON facet.fileder_id = fileder.id | |
121 | WHERE fileder.path = ?', path | |
122 | coroutine.yield name, type | |
123 | ||
124 | load_facet: (path, name, type) => | |
125 | v = @fetch_one 'SELECT facet.value | |
126 | FROM facet | |
127 | INNER JOIN fileder ON facet.fileder_id = fileder.id | |
128 | WHERE fileder.path = :path | |
129 | AND facet.name = :name | |
130 | AND facet.type = :type', | |
131 | { :path, :name, :type } | |
132 | v and v[1] | |
133 | ||
134 | create_facet: (path, name, type, blob) => | |
135 | @log "creating facet #{path} | #{name}: #{type}" | |
136 | @exec 'INSERT INTO facet (fileder_id, name, type, value) | |
137 | SELECT id, :name, :type, :blob | |
138 | FROM fileder | |
139 | WHERE fileder.path = :path', | |
140 | { :path, :name, :type, :blob } | |
141 | ||
142 | changes = @fetch_one 'SELECT changes()' | |
143 | assert changes[1] == 1, "couldn't create facet - fileder missing?" | |
144 | ||
145 | remove_facet: (path, name, type) => | |
146 | @log "removing facet #{path} | #{name}: #{type}" | |
147 | @exec 'DELETE FROM facet | |
148 | WHERE name = :name | |
149 | AND type = :type | |
150 | AND fileder_id = (SELECT id FROM fileder WHERE path = :path)', | |
151 | { :path, :name, :type } | |
152 | ||
153 | changes = @fetch_one 'SELECT changes()' | |
154 | assert changes[1] == 1, "no such facet" | |
155 | ||
156 | rename_facet: (path, name, type, next_name) => | |
157 | @log "renaming facet #{path} | #{name}: #{type} -> #{next_name}" | |
158 | @exec 'UPDATE facet | |
159 | SET name = :next_name | |
160 | WHERE name = :name | |
161 | AND type = :type | |
162 | AND fileder_id = (SELECT id FROM fileder WHERE path = :path)', | |
163 | { :path, :name, :next_name, :type } | |
164 | ||
165 | changes = @fetch_one 'SELECT changes()' | |
166 | assert changes[1] == 1, "no such facet" | |
167 | ||
168 | update_facet: (path, name, type, blob) => | |
169 | @log "updating facet #{path} | #{name}: #{type}" | |
170 | @exec 'UPDATE facet | |
171 | SET value = :blob | |
172 | WHERE facet.name = :name | |
173 | AND facet.type = :type | |
174 | AND facet.fileder_id = (SELECT id FROM fileder WHERE path = :path)', | |
175 | { :path, :name, :type, :blob } | |
176 | ||
177 | changes = @fetch_one 'SELECT changes()' | |
178 | assert changes[1] == 1, "no such facet" | |
179 | ||
180 | { | |
181 | :SQLStore | |
182 | } |
189 | 189 | |
190 | 190 | dir, base |
191 | 191 | |
192 | -- load tree from a driver instance | |
192 | -- load tree from a store instance | |
193 | 193 | -- optionally load subtree starting at 'root' path |
194 | 194 | load_tree = (store, root='') -> |
195 | 195 | fileders = setmetatable {}, |
0 | require = relative ..., 0 | |
1 | ||
2 | -- instantiate a store from a CLI arg | |
3 | -- e.g.: sql, lfs:/path/to/root, sql:MEMORY, sql:db.sqlite3 | |
4 | get_store = (args='sql', opts={verbose: true}) -> | |
5 | type, arg = args\match '(%w+):(.*)' | |
6 | type = arg unless type | |
7 | ||
8 | switch type\lower! | |
9 | when 'sql' | |
10 | import SQLStore from require '.sql' | |
11 | ||
12 | if arg == 'MEMORY' | |
13 | opts.memory = true | |
14 | else | |
15 | opts.name = arg | |
16 | ||
17 | SQLStore opts | |
18 | ||
19 | when 'lfs' | |
20 | import LFSStore from require '.lfs' | |
21 | ||
22 | opts.root = arg | |
23 | ||
24 | LFSStore opts | |
25 | ||
26 | else | |
27 | warn "unknown or missing value for STORE: valid types values are sql, lfs" | |
28 | os.exit 1 | |
29 | ||
30 | { | |
31 | :get_store | |
32 | } |
0 | lfs = require 'lfs' | |
1 | ||
2 | -- split filename into dirname + basename | |
3 | dir_base = (path) -> | |
4 | dir, base = path\match '(.-)([^/]-)$' | |
5 | if dir and #dir > 0 | |
6 | dir = dir\sub 1, #dir - 1 | |
7 | ||
8 | dir, base | |
9 | ||
10 | ||
11 | class LFSStore | |
12 | new: (opts = {}) => | |
13 | opts.root or= 'root' | |
14 | opts.verbose or= false | |
15 | ||
16 | if not opts.verbose | |
17 | @log = -> | |
18 | ||
19 | -- ensure path doesnt end with a slash | |
20 | @root = opts.root\match '^(.-)/?$' | |
21 | @log "opening '#{opts.root}'..." | |
22 | ||
23 | log: (...) => | |
24 | print "[DB]", ... | |
25 | ||
26 | -- fileders | |
27 | list_fileders_in: (path='') => | |
28 | coroutine.wrap -> | |
29 | for entry_name in lfs.dir @root .. path | |
30 | continue if '.' == entry_name\sub 1, 1 | |
31 | entry_path = @root .. "#{path}/#{entry_name}" | |
32 | if 'directory' == lfs.attributes entry_path, 'mode' | |
33 | coroutine.yield "#{path}/#{entry_name}" | |
34 | ||
35 | list_all_fileders: (path='') => | |
36 | coroutine.wrap -> | |
37 | for path in @list_fileders_in path | |
38 | coroutine.yield path | |
39 | for p in @list_all_fileders path | |
40 | coroutine.yield p | |
41 | ||
42 | create_fileder: (parent, name) => | |
43 | @log "creating fileder #{path}" | |
44 | path = "#{parent}/#{name}" | |
45 | assert lfs.mkdir @root .. path | |
46 | path | |
47 | ||
48 | remove_fileder: (path) => | |
49 | @log "removing fileder #{path}" | |
50 | ||
51 | rmdir = (path) -> | |
52 | for file in lfs.dir path | |
53 | continue if '.' == file\sub 1, 1 | |
54 | ||
55 | file_path = "#{path}/#{file}" | |
56 | switch lfs.attributes file_path, 'mode' | |
57 | when 'file' | |
58 | assert os.remove file_path | |
59 | when 'directory' | |
60 | assert rmdir file_path | |
61 | ||
62 | lfs.rmdir path | |
63 | ||
64 | rmdir @root .. path | |
65 | ||
66 | rename_fileder: (path, next_name) => | |
67 | @log "renaming fileder #{path} -> '#{next_name}'" | |
68 | parent, name = dir_base path | |
69 | assert os.rename path, @root .. "#{parent}/#{next_name}" | |
70 | ||
71 | move_fileder: (path, next_parent) => | |
72 | @log "moving fileder #{path} -> #{next_parent}/" | |
73 | parent, name = dir_base path | |
74 | assert os.rename @root .. path, @root .. "#{next_parent}/#{name}" | |
75 | ||
76 | -- facets | |
77 | list_facets: (path) => | |
78 | coroutine.wrap -> | |
79 | for entry_name in lfs.dir @root .. path | |
80 | entry_path = "#{@root .. path}/#{entry_name}" | |
81 | if 'file' == lfs.attributes entry_path, 'mode' | |
82 | entry_name = (entry_name\match '(.*)%.%w+') or entry_name | |
83 | entry_name = entry_name\gsub '%$', '/' | |
84 | name, type = entry_name\match '(%w+): *(.+)' | |
85 | if not name | |
86 | name = '' | |
87 | type = entry_name | |
88 | ||
89 | coroutine.yield name, type | |
90 | ||
91 | tofp: (path, name, type) => | |
92 | type = "#{name}: #{type}" if #name > 0 | |
93 | type = type\gsub '%/', '$' | |
94 | @root .. "#{path}/#{type}" | |
95 | ||
96 | locate: (path, name, type) => | |
97 | return unless lfs.attributes @root .. path, 'mode' | |
98 | ||
99 | type = type\gsub '%/', '$' | |
100 | name = "#{name}: " if #name > 0 | |
101 | name = name .. type | |
102 | name = name\gsub '([^%w])', '%%%1' | |
103 | ||
104 | local file_name | |
105 | for entry_name in lfs.dir @root .. path | |
106 | if (entry_name\match "^#{name}$") or entry_name\match "^#{name}%.%w+$" | |
107 | if file_name | |
108 | error "two files match #{name}: #{file_name} and #{entry_name}!" | |
109 | file_name = entry_name | |
110 | ||
111 | ||
112 | file_name and @root .. "#{path}/#{file_name}" | |
113 | ||
114 | load_facet: (path, name, type) => | |
115 | filepath = @locate path, name, type | |
116 | return unless filepath | |
117 | file = assert (io.open filepath, 'rb'), "couldn't open facet file '#{filepath}'" | |
118 | with file\read '*all' | |
119 | file\close! | |
120 | ||
121 | create_facet: (path, name, type, blob) => | |
122 | @log "creating facet #{path} | #{name}: #{type}" | |
123 | assert blob, "cant create facet without value!" | |
124 | ||
125 | filepath = @tofp path, name, type | |
126 | if lfs.attributes filepath, 'mode' | |
127 | error "facet file already exists!" | |
128 | ||
129 | file = assert (io.open filepath, 'wb'), "couldn't open facet file '#{filepath}'" | |
130 | file\write blob | |
131 | file\close! | |
132 | ||
133 | remove_facet: (path, name, type) => | |
134 | @log "removing facet #{path} | #{name}: #{type}" | |
135 | ||
136 | filepath = @locate path, name, type | |
137 | assert filepath, "couldn't locate facet!" | |
138 | assert os.remove filepath | |
139 | ||
140 | rename_facet: (path, name, type, next_name) => | |
141 | @log "renaming facet #{path} | #{name}: #{type} -> #{next_name}" | |
142 | filepath = @locate path, name, type | |
143 | assert filepath, "couldn't locate facet!" | |
144 | assert os.rename filepath, @tofp path, next_name, type | |
145 | ||
146 | update_facet: (path, name, type, blob) => | |
147 | @log "updating facet #{path} | #{name}: #{type}" | |
148 | filepath = @locate path, name, type | |
149 | assert filepath, "couldn't locate facet!" | |
150 | file = assert (io.open filepath, 'wb'), "couldn't open facet file '#{filepath}'" | |
151 | file\write blob | |
152 | file\close! | |
153 | ||
154 | { | |
155 | :LFSStore | |
156 | } |
0 | sqlite = require 'sqlite3' | |
1 | root = os.tmpname! | |
2 | ||
3 | class SQLStore | |
4 | new: (opts = {}) => | |
5 | opts.file or= 'db.sqlite3' | |
6 | opts.verbose or= false | |
7 | opts.memory or= false | |
8 | ||
9 | if not opts.verbose | |
10 | @log = -> | |
11 | ||
12 | if opts.memory | |
13 | @log "opening in-memory DB..." | |
14 | @db = sqlite.open_memory! | |
15 | else | |
16 | @log "opening '#{opts.file}'..." | |
17 | @db = sqlite.open opts.file | |
18 | ||
19 | assert @db\exec [[ | |
20 | PRAGMA foreign_keys = ON; | |
21 | PRAGMA case_sensitive_like = ON; | |
22 | CREATE TABLE IF NOT EXISTS fileder ( | |
23 | id INTEGER NOT NULL PRIMARY KEY, | |
24 | path TEXT NOT NULL UNIQUE, | |
25 | parent TEXT REFERENCES fileder(path) | |
26 | ON DELETE CASCADE | |
27 | ON UPDATE CASCADE | |
28 | ); | |
29 | INSERT OR IGNORE INTO fileder (path, parent) VALUES ("", NULL); | |
30 | ||
31 | CREATE TABLE IF NOT EXISTS facet ( | |
32 | fileder_id INTEGER NOT NULL | |
33 | REFERENCES fileder | |
34 | ON UPDATE CASCADE | |
35 | ON DELETE CASCADE, | |
36 | name TEXT NOT NULL, | |
37 | type TEXT NOT NULL, | |
38 | value BLOB NOT NULL, | |
39 | PRIMARY KEY (fileder_id, name, type) | |
40 | ); | |
41 | CREATE INDEX IF NOT EXISTS facet_fileder_id ON facet(fileder_id); | |
42 | CREATE INDEX IF NOT EXISTS facet_name ON facet(name); | |
43 | ]] | |
44 | ||
45 | log: (...) => | |
46 | print "[DB]", ... | |
47 | ||
48 | close: => | |
49 | @db\close! | |
50 | ||
51 | fetch: (q, ...) => | |
52 | stmt = assert @db\prepare q | |
53 | stmt\bind ... if 0 < select '#', ... | |
54 | stmt\irows! | |
55 | ||
56 | fetch_one: (q, ...) => | |
57 | stmt = assert @db\prepare q | |
58 | stmt\bind ... if 0 < select '#', ... | |
59 | stmt\first_irow! | |
60 | ||
61 | exec: (q, ...) => | |
62 | stmt = assert @db\prepare q | |
63 | stmt\bind ... if 0 < select '#', ... | |
64 | res = assert stmt\exec! | |
65 | ||
66 | -- fileders | |
67 | list_fileders_in: (path='') => | |
68 | coroutine.wrap -> | |
69 | for { path } in @fetch 'SELECT path | |
70 | FROM fileder WHERE parent IS ?', path | |
71 | coroutine.yield path | |
72 | ||
73 | list_all_fileders: (path='') => | |
74 | coroutine.wrap -> | |
75 | for path in @list_fileders_in path | |
76 | coroutine.yield path | |
77 | for p in @list_all_fileders path | |
78 | coroutine.yield p | |
79 | ||
80 | create_fileder: (parent, name) => | |
81 | path = "#{parent}/#{name}" | |
82 | ||
83 | @log "creating fileder #{path}" | |
84 | @exec 'INSERT INTO fileder (path, parent) | |
85 | VALUES (:path, :parent)', | |
86 | { :path, :parent } | |
87 | ||
88 | changes = @fetch_one 'SELECT changes()' | |
89 | assert changes[1] == 1, "couldn't create fileder - parent missing?" | |
90 | path | |
91 | ||
92 | remove_fileder: (path) => | |
93 | @log "removing fileder #{path}" | |
94 | @exec 'DELETE FROM fileder | |
95 | WHERE path LIKE :path || "/%" | |
96 | OR path = :path', path | |
97 | ||
98 | rename_fileder: (path, next_name) => | |
99 | @log "renaming fileder #{path} -> '#{next_name}'" | |
100 | error 'not implemented' | |
101 | ||
102 | @exec 'UPDATE fileder | |
103 | SET path = parent || "/" || :next_name | |
104 | WHERE path = :path', | |
105 | { :path, :next_name } | |
106 | ||
107 | -- @TODO: rename all children, child-children... | |
108 | ||
109 | move_fileder: (path, next_parent) => | |
110 | @log "moving fileder #{path} -> #{next_parent}/" | |
111 | error 'not implemented' | |
112 | ||
113 | -- @TODO: remove all children, child-children... | |
114 | ||
115 | -- facets | |
116 | list_facets: (path) => | |
117 | coroutine.wrap -> | |
118 | for { name, type } in @fetch 'SELECT facet.name, facet.type | |
119 | FROM facet | |
120 | INNER JOIN fileder ON facet.fileder_id = fileder.id | |
121 | WHERE fileder.path = ?', path | |
122 | coroutine.yield name, type | |
123 | ||
124 | load_facet: (path, name, type) => | |
125 | v = @fetch_one 'SELECT facet.value | |
126 | FROM facet | |
127 | INNER JOIN fileder ON facet.fileder_id = fileder.id | |
128 | WHERE fileder.path = :path | |
129 | AND facet.name = :name | |
130 | AND facet.type = :type', | |
131 | { :path, :name, :type } | |
132 | v and v[1] | |
133 | ||
134 | create_facet: (path, name, type, blob) => | |
135 | @log "creating facet #{path} | #{name}: #{type}" | |
136 | @exec 'INSERT INTO facet (fileder_id, name, type, value) | |
137 | SELECT id, :name, :type, :blob | |
138 | FROM fileder | |
139 | WHERE fileder.path = :path', | |
140 | { :path, :name, :type, :blob } | |
141 | ||
142 | changes = @fetch_one 'SELECT changes()' | |
143 | assert changes[1] == 1, "couldn't create facet - fileder missing?" | |
144 | ||
145 | remove_facet: (path, name, type) => | |
146 | @log "removing facet #{path} | #{name}: #{type}" | |
147 | @exec 'DELETE FROM facet | |
148 | WHERE name = :name | |
149 | AND type = :type | |
150 | AND fileder_id = (SELECT id FROM fileder WHERE path = :path)', | |
151 | { :path, :name, :type } | |
152 | ||
153 | changes = @fetch_one 'SELECT changes()' | |
154 | assert changes[1] == 1, "no such facet" | |
155 | ||
156 | rename_facet: (path, name, type, next_name) => | |
157 | @log "renaming facet #{path} | #{name}: #{type} -> #{next_name}" | |
158 | @exec 'UPDATE facet | |
159 | SET name = :next_name | |
160 | WHERE name = :name | |
161 | AND type = :type | |
162 | AND fileder_id = (SELECT id FROM fileder WHERE path = :path)', | |
163 | { :path, :name, :next_name, :type } | |
164 | ||
165 | changes = @fetch_one 'SELECT changes()' | |
166 | assert changes[1] == 1, "no such facet" | |
167 | ||
168 | update_facet: (path, name, type, blob) => | |
169 | @log "updating facet #{path} | #{name}: #{type}" | |
170 | @exec 'UPDATE facet | |
171 | SET value = :blob | |
172 | WHERE facet.name = :name | |
173 | AND facet.type = :type | |
174 | AND facet.fileder_id = (SELECT id FROM fileder WHERE path = :path)', | |
175 | { :path, :name, :type, :blob } | |
176 | ||
177 | changes = @fetch_one 'SELECT changes()' | |
178 | assert changes[1] == 1, "no such facet" | |
179 | ||
180 | { | |
181 | :SQLStore | |
182 | } |