owebjs 1.3.1 → 1.3.2
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.
- package/README.md +37 -0
- package/dist/index.d.ts +7 -2
- package/dist/structures/Oweb.js +17 -7
- package/dist/utils/assignRoutes.js +101 -20
- package/dist/utils/utils.js +14 -1
- package/dist/utils/{watchRoutes.js → watcher.js} +4 -4
- package/dist/uws_darwin_arm64_108.node +0 -0
- package/dist/uws_darwin_arm64_83.node +0 -0
- package/dist/uws_darwin_arm64_93.node +0 -0
- package/dist/uws_darwin_x64_108.node +0 -0
- package/dist/uws_darwin_x64_83.node +0 -0
- package/dist/uws_darwin_x64_93.node +0 -0
- package/dist/uws_linux_arm64_108.node +0 -0
- package/dist/uws_linux_arm64_83.node +0 -0
- package/dist/uws_linux_arm64_93.node +0 -0
- package/dist/uws_linux_x64_108.node +0 -0
- package/dist/uws_linux_x64_83.node +0 -0
- package/dist/uws_linux_x64_93.node +0 -0
- package/dist/uws_win32_x64_108.node +0 -0
- package/dist/uws_win32_x64_83.node +0 -0
- package/dist/uws_win32_x64_93.node +0 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -79,6 +79,43 @@ export default class extends Route {
|
|
|
79
79
|
|
|
80
80
|
This will create a GET route at `/users/:id`.
|
|
81
81
|
|
|
82
|
+
### Parameter Validation with Matchers
|
|
83
|
+
|
|
84
|
+
Use matchers to validate dynamic route parameters:
|
|
85
|
+
|
|
86
|
+
```javascript
|
|
87
|
+
// routes/users/[id=integer].js
|
|
88
|
+
import { Route } from 'owebjs';
|
|
89
|
+
|
|
90
|
+
export default class extends Route {
|
|
91
|
+
async handle(req, res) {
|
|
92
|
+
res.send({ userId: req.params.id });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```javascript
|
|
98
|
+
// matchers/integer.js
|
|
99
|
+
export default function (val) {
|
|
100
|
+
return !isNaN(val);
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Then configure Oweb to use your matchers directory:
|
|
105
|
+
|
|
106
|
+
```javascript
|
|
107
|
+
await app.loadRoutes({
|
|
108
|
+
directory: 'routes',
|
|
109
|
+
matchersDirectory: 'matchers', // Directory containing custom matchers
|
|
110
|
+
hmr: {
|
|
111
|
+
enabled: true,
|
|
112
|
+
matchersDirectory: 'matchers', // Optional: Enable HMR for matchers
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Now you can use your custom matchers in route filenames with the syntax `[paramName=matcherName]`.
|
|
118
|
+
|
|
82
119
|
### HTTP Methods
|
|
83
120
|
|
|
84
121
|
Specify the HTTP method in the filename:
|
package/dist/index.d.ts
CHANGED
|
@@ -24,11 +24,13 @@ interface OwebOptions extends FastifyServerOptions {
|
|
|
24
24
|
}
|
|
25
25
|
interface LoadRoutesOptions {
|
|
26
26
|
directory: string;
|
|
27
|
+
matchersDirectory?: string;
|
|
27
28
|
hmr?: {
|
|
28
29
|
/**
|
|
29
30
|
* The directory to watch for changes. If not specified, it will use the routes directory.
|
|
30
31
|
*/
|
|
31
32
|
directory?: string;
|
|
33
|
+
matchersDirectory?: string;
|
|
32
34
|
enabled: boolean;
|
|
33
35
|
};
|
|
34
36
|
}
|
|
@@ -39,6 +41,7 @@ declare class _FastifyInstance {
|
|
|
39
41
|
declare class Oweb extends _FastifyInstance {
|
|
40
42
|
_options: OwebOptions;
|
|
41
43
|
private hmrDirectory;
|
|
44
|
+
private hmrMatchersDirectory;
|
|
42
45
|
routes: Map<string, any>;
|
|
43
46
|
constructor(options?: OwebOptions);
|
|
44
47
|
/**
|
|
@@ -49,11 +52,13 @@ declare class Oweb extends _FastifyInstance {
|
|
|
49
52
|
/**
|
|
50
53
|
* Loads routes from a directory.
|
|
51
54
|
* @param options.directory The directory to load routes from.
|
|
55
|
+
* @param options.matchersDirectory The directory to load matchers from.
|
|
52
56
|
* @param options.hmr Configuration for Hot Module Replacement.
|
|
53
57
|
* @param options.hmr.enabled Whether to enable HMR. HMR is disabled if NODE_ENV is set to production.
|
|
54
|
-
* @param options.hmr.directory The directory to watch for changes. If not specified, it will use the routes directory.
|
|
58
|
+
* @param options.hmr.directory The directory to watch for route changes. If not specified, it will use the routes directory.
|
|
59
|
+
* @param options.hmr.matchersDirectory The directory to watch for matcher changes. If not specified, it will use the matchers directory.
|
|
55
60
|
*/
|
|
56
|
-
loadRoutes({ directory, hmr }: LoadRoutesOptions): Promise<void>;
|
|
61
|
+
loadRoutes({ directory, matchersDirectory, hmr }: LoadRoutesOptions): Promise<void>;
|
|
57
62
|
/**
|
|
58
63
|
*
|
|
59
64
|
* Watches for changes in the routes directory
|
package/dist/structures/Oweb.js
CHANGED
|
@@ -2,8 +2,8 @@ import {
|
|
|
2
2
|
__name
|
|
3
3
|
} from "../chunk-SHUYVCID.js";
|
|
4
4
|
import Fastify from "fastify";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { applyMatcherHMR, applyRouteHMR, assignRoutes } from '../utils/assignRoutes.js';
|
|
6
|
+
import { watchDirectory } from '../utils/watcher.js';
|
|
7
7
|
import { info, success, warn } from '../utils/logger.js';
|
|
8
8
|
let _FastifyInstance = class _FastifyInstance2 {
|
|
9
9
|
static {
|
|
@@ -16,6 +16,7 @@ class Oweb extends _FastifyInstance {
|
|
|
16
16
|
}
|
|
17
17
|
_options = {};
|
|
18
18
|
hmrDirectory;
|
|
19
|
+
hmrMatchersDirectory;
|
|
19
20
|
routes = /* @__PURE__ */ new Map();
|
|
20
21
|
constructor(options) {
|
|
21
22
|
super();
|
|
@@ -60,28 +61,37 @@ class Oweb extends _FastifyInstance {
|
|
|
60
61
|
/**
|
|
61
62
|
* Loads routes from a directory.
|
|
62
63
|
* @param options.directory The directory to load routes from.
|
|
64
|
+
* @param options.matchersDirectory The directory to load matchers from.
|
|
63
65
|
* @param options.hmr Configuration for Hot Module Replacement.
|
|
64
66
|
* @param options.hmr.enabled Whether to enable HMR. HMR is disabled if NODE_ENV is set to production.
|
|
65
|
-
* @param options.hmr.directory The directory to watch for changes. If not specified, it will use the routes directory.
|
|
67
|
+
* @param options.hmr.directory The directory to watch for route changes. If not specified, it will use the routes directory.
|
|
68
|
+
* @param options.hmr.matchersDirectory The directory to watch for matcher changes. If not specified, it will use the matchers directory.
|
|
66
69
|
*/
|
|
67
|
-
loadRoutes({ directory, hmr }) {
|
|
70
|
+
loadRoutes({ directory, matchersDirectory, hmr }) {
|
|
68
71
|
if (hmr && !hmr.directory) hmr.directory = directory;
|
|
72
|
+
if (hmr && !hmr.matchersDirectory) hmr.matchersDirectory = matchersDirectory;
|
|
69
73
|
if (hmr?.enabled) {
|
|
70
74
|
this.hmrDirectory = hmr.directory;
|
|
75
|
+
this.hmrMatchersDirectory = hmr.matchersDirectory;
|
|
71
76
|
success(`Hot Module Replacement enabled. Watching changes in ${hmr.directory}`, "HMR");
|
|
72
77
|
} else {
|
|
73
78
|
warn('Hot Module Replacement is disabled. Use "await app.loadRoutes({ hmr: { enabled: true, directory: path } })" to enable it.', "HMR");
|
|
74
79
|
}
|
|
75
|
-
return assignRoutes(this, directory);
|
|
80
|
+
return assignRoutes(this, directory, matchersDirectory);
|
|
76
81
|
}
|
|
77
82
|
/**
|
|
78
83
|
*
|
|
79
84
|
* Watches for changes in the routes directory
|
|
80
85
|
*/
|
|
81
86
|
watch() {
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
watchDirectory(this.hmrDirectory, true, (op, path, content) => {
|
|
88
|
+
applyRouteHMR(this, op, this.hmrDirectory, path, content);
|
|
84
89
|
});
|
|
90
|
+
if (this.hmrMatchersDirectory) {
|
|
91
|
+
watchDirectory(this.hmrMatchersDirectory, true, (op, path, content) => {
|
|
92
|
+
applyMatcherHMR(this, op, this.hmrMatchersDirectory, path, content);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
85
95
|
}
|
|
86
96
|
/**
|
|
87
97
|
*
|
|
@@ -9,7 +9,9 @@ import { walk } from './walk.js';
|
|
|
9
9
|
import { success, warn } from './logger.js';
|
|
10
10
|
import { match } from "path-to-regexp";
|
|
11
11
|
import generateFunctionFromTypescript from './generateFunctionFromTypescript.js';
|
|
12
|
+
import { readdirSync } from "node:fs";
|
|
12
13
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
let matcherOverrides = {};
|
|
13
15
|
let routeFunctions = {};
|
|
14
16
|
const temporaryRequests = {
|
|
15
17
|
get: {},
|
|
@@ -21,7 +23,41 @@ const temporaryRequests = {
|
|
|
21
23
|
};
|
|
22
24
|
let routesCache = [];
|
|
23
25
|
const compiledRoutes = {};
|
|
24
|
-
|
|
26
|
+
function removeExtension(filePath) {
|
|
27
|
+
const lastDotIndex = filePath.lastIndexOf(".");
|
|
28
|
+
if (lastDotIndex !== -1) {
|
|
29
|
+
return filePath.substring(0, lastDotIndex);
|
|
30
|
+
}
|
|
31
|
+
return filePath;
|
|
32
|
+
}
|
|
33
|
+
__name(removeExtension, "removeExtension");
|
|
34
|
+
const applyMatcherHMR = /* @__PURE__ */ __name(async (oweb, op, workingDir, filePath, content) => {
|
|
35
|
+
let def;
|
|
36
|
+
const fileName = path.basename(filePath);
|
|
37
|
+
if (op === "delete-file") {
|
|
38
|
+
delete matcherOverrides[removeExtension(fileName)];
|
|
39
|
+
success(`Matcher ${filePath} removed in 0ms`, "HMR");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (filePath.endsWith(".ts")) {
|
|
43
|
+
const start = Date.now();
|
|
44
|
+
def = content.length ? await generateFunctionFromTypescript(content, filePath) : void 0;
|
|
45
|
+
const end = Date.now() - start;
|
|
46
|
+
success(`Matcher ${filePath} compiled and reloaded in ${end}ms`, "HMR");
|
|
47
|
+
} else {
|
|
48
|
+
const start = Date.now();
|
|
49
|
+
const newFilePath = filePath.replaceAll("\\", "/");
|
|
50
|
+
const packageURL = new URL(path.resolve(newFilePath), `file://${__dirname}`).pathname.replaceAll("\\", "/");
|
|
51
|
+
const cacheBuster = `?t=${Date.now()}`;
|
|
52
|
+
def = (await import(packageURL + cacheBuster)).default;
|
|
53
|
+
const end = Date.now() - start;
|
|
54
|
+
success(`Matcher ${filePath} reloaded in ${end}ms`, "HMR");
|
|
55
|
+
}
|
|
56
|
+
if (def) {
|
|
57
|
+
matcherOverrides[removeExtension(fileName)] = def;
|
|
58
|
+
}
|
|
59
|
+
}, "applyMatcherHMR");
|
|
60
|
+
const applyRouteHMR = /* @__PURE__ */ __name(async (oweb, op, workingDir, path2, content) => {
|
|
25
61
|
if (path2.endsWith("hooks.js") || path2.endsWith("hooks.ts")) {
|
|
26
62
|
warn(`Hot Module Replacement is not supported for hooks. Restart the server for changes to take effect.`, "HMR");
|
|
27
63
|
return;
|
|
@@ -65,7 +101,7 @@ const applyHMR = /* @__PURE__ */ __name(async (oweb, op, workingDir, path2, cont
|
|
|
65
101
|
const end = Date.now() - start;
|
|
66
102
|
success(`Route ${f.method.toUpperCase()}:${f.url} removed in ${end}ms`, "HMR");
|
|
67
103
|
}
|
|
68
|
-
}, "
|
|
104
|
+
}, "applyRouteHMR");
|
|
69
105
|
const generateRoutes = /* @__PURE__ */ __name(async (files) => {
|
|
70
106
|
const routes = [];
|
|
71
107
|
for (const file of files) {
|
|
@@ -78,6 +114,7 @@ const generateRoutes = /* @__PURE__ */ __name(async (files) => {
|
|
|
78
114
|
routes.push({
|
|
79
115
|
url: route.url,
|
|
80
116
|
method: route.method,
|
|
117
|
+
matchers: route.matchers,
|
|
81
118
|
fn: compiledRoutes[file.filePath],
|
|
82
119
|
fileInfo: file
|
|
83
120
|
});
|
|
@@ -89,6 +126,7 @@ const generateRoutes = /* @__PURE__ */ __name(async (files) => {
|
|
|
89
126
|
routes.push({
|
|
90
127
|
url: route.url,
|
|
91
128
|
method: route.method,
|
|
129
|
+
matchers: route.matchers,
|
|
92
130
|
fn: routeFuncs,
|
|
93
131
|
fileInfo: file
|
|
94
132
|
});
|
|
@@ -100,7 +138,18 @@ function inner(oweb, route) {
|
|
|
100
138
|
return;
|
|
101
139
|
}
|
|
102
140
|
const routeFunc = new route.fn();
|
|
141
|
+
const matchers = route.matchers;
|
|
103
142
|
return function(req, res) {
|
|
143
|
+
const checkMatchers = /* @__PURE__ */ __name(() => {
|
|
144
|
+
for (const matcher of matchers) {
|
|
145
|
+
const param = req.params[matcher.paramName];
|
|
146
|
+
const fun = matcherOverrides[matcher.matcherName];
|
|
147
|
+
if (fun) {
|
|
148
|
+
return fun(param);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return true;
|
|
152
|
+
}, "checkMatchers");
|
|
104
153
|
const handle = /* @__PURE__ */ __name(() => {
|
|
105
154
|
if (routeFunc.handle.constructor.name == "AsyncFunction") {
|
|
106
155
|
routeFunc.handle(req, res).catch((error) => {
|
|
@@ -127,16 +176,32 @@ function inner(oweb, route) {
|
|
|
127
176
|
const hookFun = route.fileInfo.hooks[index];
|
|
128
177
|
hookFun.prototype.handle(req, res, () => {
|
|
129
178
|
if (index + 1 == route.fileInfo.hooks.length) {
|
|
130
|
-
|
|
179
|
+
if (!checkMatchers()) {
|
|
180
|
+
send404(req, res);
|
|
181
|
+
} else {
|
|
182
|
+
handle();
|
|
183
|
+
}
|
|
131
184
|
}
|
|
132
185
|
});
|
|
133
186
|
}
|
|
134
187
|
} else {
|
|
135
|
-
|
|
188
|
+
if (!checkMatchers()) {
|
|
189
|
+
send404(req, res);
|
|
190
|
+
} else {
|
|
191
|
+
handle();
|
|
192
|
+
}
|
|
136
193
|
}
|
|
137
194
|
};
|
|
138
195
|
}
|
|
139
196
|
__name(inner, "inner");
|
|
197
|
+
function send404(req, res) {
|
|
198
|
+
return res.status(404).send({
|
|
199
|
+
message: `Route ${req.method}:${req.url} not found`,
|
|
200
|
+
error: "Not Found",
|
|
201
|
+
statusCode: 404
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
__name(send404, "send404");
|
|
140
205
|
function assignSpecificRoute(oweb, route) {
|
|
141
206
|
if (!route.fn) return;
|
|
142
207
|
const routeFunc = new route.fn();
|
|
@@ -145,16 +210,39 @@ function assignSpecificRoute(oweb, route) {
|
|
|
145
210
|
if (routeFunctions[route.fileInfo.filePath]) {
|
|
146
211
|
return routeFunctions[route.fileInfo.filePath](req, res);
|
|
147
212
|
} else {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
213
|
+
const vals = temporaryRequests[req.method.toLowerCase()];
|
|
214
|
+
const keys = Object.keys(vals);
|
|
215
|
+
if (!vals || !keys.length) {
|
|
216
|
+
return send404(req, res);
|
|
217
|
+
}
|
|
218
|
+
const f = keys.find((tempName) => {
|
|
219
|
+
const matcher = match(tempName);
|
|
220
|
+
return matcher(req.url);
|
|
152
221
|
});
|
|
222
|
+
if (f && vals[f]) {
|
|
223
|
+
return vals[f](req, res);
|
|
224
|
+
} else {
|
|
225
|
+
return send404(req, res);
|
|
226
|
+
}
|
|
153
227
|
}
|
|
154
228
|
});
|
|
155
229
|
}
|
|
156
230
|
__name(assignSpecificRoute, "assignSpecificRoute");
|
|
157
|
-
|
|
231
|
+
async function loadMatchers(directoryPath) {
|
|
232
|
+
const files = readdirSync(directoryPath);
|
|
233
|
+
for (const file of files) {
|
|
234
|
+
const filePath = path.join(directoryPath, file).replaceAll("\\", "/");
|
|
235
|
+
const fileName = path.basename(filePath);
|
|
236
|
+
const packageURL = new URL(path.resolve(filePath), `file://${__dirname}`).pathname.replaceAll("\\", "/");
|
|
237
|
+
const def = await import(packageURL);
|
|
238
|
+
matcherOverrides[removeExtension(fileName)] = def.default;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
__name(loadMatchers, "loadMatchers");
|
|
242
|
+
const assignRoutes = /* @__PURE__ */ __name(async (oweb, directory, matchersDirectory) => {
|
|
243
|
+
if (matchersDirectory) {
|
|
244
|
+
loadMatchers(matchersDirectory);
|
|
245
|
+
}
|
|
158
246
|
const files = await walk(directory);
|
|
159
247
|
const routes = await generateRoutes(files);
|
|
160
248
|
routesCache = routes;
|
|
@@ -162,11 +250,7 @@ const assignRoutes = /* @__PURE__ */ __name(async (oweb, directory) => {
|
|
|
162
250
|
const vals = temporaryRequests[req.method.toLowerCase()];
|
|
163
251
|
const keys = Object.keys(vals);
|
|
164
252
|
if (!vals || !keys.length) {
|
|
165
|
-
return res
|
|
166
|
-
message: `Route ${req.method}:${req.url} not found`,
|
|
167
|
-
error: "Not Found",
|
|
168
|
-
statusCode: 404
|
|
169
|
-
});
|
|
253
|
+
return send404(req, res);
|
|
170
254
|
}
|
|
171
255
|
const f = keys.find((tempName) => {
|
|
172
256
|
const matcher = match(tempName);
|
|
@@ -175,11 +259,7 @@ const assignRoutes = /* @__PURE__ */ __name(async (oweb, directory) => {
|
|
|
175
259
|
if (f && vals[f]) {
|
|
176
260
|
return vals[f](req, res);
|
|
177
261
|
} else {
|
|
178
|
-
return res
|
|
179
|
-
message: `Route ${req.method}:${req.url} not found`,
|
|
180
|
-
error: "Not Found",
|
|
181
|
-
statusCode: 404
|
|
182
|
-
});
|
|
262
|
+
return send404(req, res);
|
|
183
263
|
}
|
|
184
264
|
});
|
|
185
265
|
for (const route of routes) {
|
|
@@ -187,7 +267,8 @@ const assignRoutes = /* @__PURE__ */ __name(async (oweb, directory) => {
|
|
|
187
267
|
}
|
|
188
268
|
}, "assignRoutes");
|
|
189
269
|
export {
|
|
190
|
-
|
|
270
|
+
applyMatcherHMR,
|
|
271
|
+
applyRouteHMR,
|
|
191
272
|
assignRoutes,
|
|
192
273
|
generateRoutes
|
|
193
274
|
};
|
package/dist/utils/utils.js
CHANGED
|
@@ -28,6 +28,18 @@ const buildRouteURL = /* @__PURE__ */ __name((path) => {
|
|
|
28
28
|
let method = "get";
|
|
29
29
|
const paramURL = convertParamSyntax(normalizedPath);
|
|
30
30
|
let url = convertCatchallSyntax(paramURL);
|
|
31
|
+
const matcherSplit = url.split("/");
|
|
32
|
+
const matcherFilter = matcherSplit.filter((x) => x.startsWith(":"));
|
|
33
|
+
const matchers = matcherFilter.map((x) => {
|
|
34
|
+
const split = x.split("=");
|
|
35
|
+
return {
|
|
36
|
+
paramName: split[0].slice(1),
|
|
37
|
+
matcherName: split[1]
|
|
38
|
+
};
|
|
39
|
+
}).filter((x) => x.matcherName);
|
|
40
|
+
for (const matcher of matchers) {
|
|
41
|
+
url = url.replace(`:${matcher.paramName}=${matcher.matcherName}`, `:${matcher.paramName}`);
|
|
42
|
+
}
|
|
31
43
|
for (const m of [
|
|
32
44
|
".DELETE",
|
|
33
45
|
".POST",
|
|
@@ -43,7 +55,8 @@ const buildRouteURL = /* @__PURE__ */ __name((path) => {
|
|
|
43
55
|
}
|
|
44
56
|
return {
|
|
45
57
|
url,
|
|
46
|
-
method
|
|
58
|
+
method,
|
|
59
|
+
matchers
|
|
47
60
|
};
|
|
48
61
|
}, "buildRouteURL");
|
|
49
62
|
export {
|
|
@@ -3,11 +3,11 @@ import {
|
|
|
3
3
|
} from "../chunk-SHUYVCID.js";
|
|
4
4
|
import chokidar from "chokidar";
|
|
5
5
|
import { readFileSync } from "fs";
|
|
6
|
-
function
|
|
6
|
+
function watchDirectory(dir, ignoreInitial = true, onUpdate) {
|
|
7
7
|
const watcher = chokidar.watch(dir, {
|
|
8
8
|
ignored: /([/\\]\.)|(node_modules)|(dist)/,
|
|
9
9
|
persistent: true,
|
|
10
|
-
ignoreInitial
|
|
10
|
+
ignoreInitial,
|
|
11
11
|
awaitWriteFinish: {
|
|
12
12
|
stabilityThreshold: 150,
|
|
13
13
|
pollInterval: 50
|
|
@@ -25,7 +25,7 @@ function watchRoutes(dir, onUpdate) {
|
|
|
25
25
|
onUpdate("delete-file", filePath, "");
|
|
26
26
|
});
|
|
27
27
|
}
|
|
28
|
-
__name(
|
|
28
|
+
__name(watchDirectory, "watchDirectory");
|
|
29
29
|
export {
|
|
30
|
-
|
|
30
|
+
watchDirectory
|
|
31
31
|
};
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|