v2 / vlib / veb / assets / assets.v
312 lines · 270 sloc · 8.06 KB · 8e35f4d9848f7ad35d857a187dddbfd2eca5e19d
Raw
1module assets
2
3import crypto.md5
4import os
5import strings
6import time
7import veb
8
9pub enum AssetType {
10 css
11 js
12 all
13}
14
15pub struct Asset {
16pub:
17 kind AssetType
18 file_path string
19 last_modified time.Time
20 include_name string
21}
22
23pub struct AssetManager {
24mut:
25 css []Asset
26 js []Asset
27 cached_file_names []string
28pub mut:
29 // when true assets will be minified
30 minify bool
31 // the directory to store the cached/combined files
32 cache_dir string
33 // how a combined file should be named. For example for css the extension '.css'
34 // will be added to the end of `combined_file_name`
35 combined_file_name string = 'combined'
36}
37
38fn (mut am AssetManager) add_asset_directory(directory_path string, traversed_path string) ! {
39 files := os.ls(directory_path)!
40 if files.len > 0 {
41 for file in files {
42 full_path := os.join_path(directory_path, file)
43 relative_path := os.join_path(traversed_path, file)
44
45 if os.is_dir(full_path) {
46 am.add_asset_directory(full_path, relative_path)!
47 } else {
48 ext := os.file_ext(full_path)
49 match ext {
50 '.css' { am.add(.css, full_path, relative_path)! }
51 '.js' { am.add(.js, full_path, relative_path)! }
52 // ignore non css/js files
53 else {}
54 }
55 }
56 }
57 }
58}
59
60// handle_assets recursively walks `directory_path` and adds any assets to the asset manager
61pub fn (mut am AssetManager) handle_assets(directory_path string) ! {
62 return am.add_asset_directory(directory_path, '')
63}
64
65// handle_assets_at recursively walks `directory_path` and adds any assets to the asset manager.
66// The include name of assets are prefixed with `prepend`
67pub fn (mut am AssetManager) handle_assets_at(directory_path string, prepend string) ! {
68 // remove trailing '/'
69 return am.add_asset_directory(directory_path, prepend.trim_right('/'))
70}
71
72// get all assets of type `asset_type`
73pub fn (am AssetManager) get_assets(asset_type AssetType) []Asset {
74 return match asset_type {
75 .css {
76 am.css
77 }
78 .js {
79 am.js
80 }
81 .all {
82 mut assets := []Asset{}
83 assets << am.css
84 assets << am.js
85 assets
86 }
87 }
88}
89
90// add an asset to the asset manager
91pub fn (mut am AssetManager) add(asset_type AssetType, file_path string, include_name string) ! {
92 if asset_type == .all {
93 return error('cannot minify asset of type "all"')
94 }
95 if !os.exists(file_path) {
96 return error('cannot add asset: file "${file_path}" does not exist')
97 }
98
99 last_modified_unix := os.file_last_mod_unix(file_path)
100
101 mut real_path := file_path
102
103 if am.minify {
104 // minify and cache file if it was modified
105 output_path, is_cached := am.minify_and_cache(asset_type, real_path, last_modified_unix,
106 include_name)!
107
108 if is_cached == false && am.exists(asset_type, include_name) {
109 // file was not modified between the last call to `add`
110 // and the file was already in the asset manager, so we don't need to
111 // add it again
112 return
113 }
114
115 real_path = output_path
116 }
117
118 asset := Asset{
119 kind: asset_type
120 file_path: real_path
121 last_modified: time.unix(last_modified_unix)
122 include_name: include_name
123 }
124
125 match asset_type {
126 .css { am.css << asset }
127 .js { am.js << asset }
128 else {}
129 }
130}
131
132fn (mut am AssetManager) minify_and_cache(asset_type AssetType, file_path string, last_modified i64, include_name string) !(string, bool) {
133 if asset_type == .all {
134 return error('cannot minify asset of type "all"')
135 }
136
137 if am.cache_dir == '' {
138 return error('cannot minify asset: cache directory is not valid')
139 } else if !os.exists(am.cache_dir) {
140 os.mkdir_all(am.cache_dir)!
141 }
142
143 cache_key := am.get_cache_key(file_path, last_modified)
144 output_file := '${cache_key}.${asset_type}'
145 output_path := os.join_path(am.cache_dir, output_file)
146
147 if os.exists(output_path) {
148 // the output path already exists, this means that the file has
149 // been minifed and cached before and hasn't changed in the meantime
150 am.cached_file_names << output_file
151 return output_path, false
152 } else {
153 // check if the file has been minified before, but is modified.
154 // if that's the case we remove the old cached file
155 cached_files := os.ls(am.cache_dir)!
156 hash := cache_key.all_before('-')
157 for file in cached_files {
158 if file.starts_with(hash) {
159 os.rm(os.join_path(am.cache_dir, file))!
160 }
161 }
162 }
163
164 txt := os.read_file(file_path)!
165 minified := match asset_type {
166 .css { minify_css(txt) }
167 .js { minify_js(txt) }
168 else { '' }
169 }
170
171 os.write_file(output_path, minified)!
172
173 am.cached_file_names << output_file
174 return output_path, true
175}
176
177fn (mut am AssetManager) get_cache_key(file_path string, last_modified i64) string {
178 abs_path := if os.is_abs_path(file_path) { file_path } else { os.resource_abs_path(file_path) }
179 hash := md5.sum(abs_path.bytes())
180 return '${hash.hex()}-${last_modified}'
181}
182
183// cleanup_cache removes all files in the cache directory that aren't cached at the time
184// this function is called
185pub fn (mut am AssetManager) cleanup_cache() ! {
186 if am.cache_dir == '' {
187 return error('[veb.assets]: cache directory is not valid')
188 }
189 cached_files := os.ls(am.cache_dir)!
190
191 // loop over all the files in the cache directory. If a file isn't cached, remove it
192 for file in cached_files {
193 ext := os.file_ext(file)
194 if ext !in ['.css', '.js'] || file in am.cached_file_names {
195 continue
196 } else if !file.starts_with(am.combined_file_name) {
197 os.rm(os.join_path(am.cache_dir, file))!
198 }
199 }
200}
201
202// check if an asset is already added to the asset manager
203pub fn (am AssetManager) exists(asset_type AssetType, include_name string) bool {
204 assets := am.get_assets(asset_type)
205
206 return assets.any(it.include_name == include_name)
207}
208
209// include css/js files in your veb app from templates
210// Usage example:
211// ```html
212// @{app.am.include(.css, 'main.css')}
213// ```
214pub fn (am AssetManager) include(asset_type AssetType, include_name string) veb.RawHtml {
215 assets := am.get_assets(asset_type)
216 for asset in assets {
217 if asset.include_name == include_name {
218 // always add link/src from root of web server ('/css/main.css'),
219 // but leave absolute paths intact
220 mut real_path := asset.file_path
221 if real_path[0] != `/` && !os.is_abs_path(real_path) {
222 real_path = '/${asset.file_path}'
223 }
224
225 return match asset_type {
226 .css {
227 '<link rel="stylesheet" href="${real_path}">'
228 }
229 .js {
230 '<script src="${real_path}"></script>'
231 }
232 else {
233 eprintln('[veb.assets] can only include css or js assets')
234 ''
235 }
236 }
237 }
238 }
239 eprintln('[veb.assets] no asset with include name "${include_name}" exists!')
240 return ''
241}
242
243// combine assets of type `asset_type` into a single file and return the outputted file path.
244// If you call `combine` with asset type `all` the function will return an empty string,
245// the minified files will be available at `combined_file_name`.`asset_type`
246pub fn (mut am AssetManager) combine(asset_type AssetType) !string {
247 if asset_type == .all {
248 am.combine(.css)!
249 am.combine(.js)!
250 return ''
251 }
252 if am.cache_dir == '' {
253 return error('cannot combine assets: cache directory is not valid')
254 } else if !os.exists(am.cache_dir) {
255 os.mkdir_all(am.cache_dir)!
256 }
257
258 assets := am.get_assets(asset_type)
259 combined_file_path := os.join_path(am.cache_dir, '${am.combined_file_name}.${asset_type}')
260 mut f := os.create(combined_file_path)!
261
262 for asset in assets {
263 bytes := os.read_bytes(asset.file_path)!
264 f.write(bytes)!
265 f.write_string('\n')!
266 }
267
268 f.close()
269
270 return combined_file_path
271}
272
273// TODO: implement proper minification
274@[manualfree]
275pub fn minify_css(css string) string {
276 mut lines := css.split('\n')
277 // estimate arbitrary number of characters for a line of css
278 mut sb := strings.new_builder(lines.len * 20)
279 defer {
280 unsafe { sb.free() }
281 }
282
283 for line in lines {
284 trimmed := line.trim_space()
285 if trimmed != '' {
286 sb.write_string(trimmed)
287 }
288 }
289
290 return sb.str()
291}
292
293// TODO: implement proper minification
294@[manualfree]
295pub fn minify_js(js string) string {
296 mut lines := js.split('\n')
297 // estimate arbitrary number of characters for a line of js
298 mut sb := strings.new_builder(lines.len * 40)
299 defer {
300 unsafe { sb.free() }
301 }
302
303 for line in lines {
304 trimmed := line.trim_space()
305 if trimmed != '' {
306 sb.write_string(trimmed)
307 sb.write_u8(` `)
308 }
309 }
310
311 return sb.str()
312}
313