mirror of
https://github.com/cool-team-official/cool-admin-midway-packages.git
synced 2025-12-12 06:12:48 +00:00
更换缓存到midway官方最新
This commit is contained in:
parent
ea9d43bbce
commit
0b4546e965
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@cool-midway/core",
|
"name": "@cool-midway/core",
|
||||||
"version": "7.1.4",
|
"version": "7.1.6",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"typings": "index.d.ts",
|
"typings": "index.d.ts",
|
||||||
@ -46,7 +46,9 @@
|
|||||||
"typescript": "~4.9.4"
|
"typescript": "~4.9.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@midwayjs/cache": "^3.9.0",
|
"@cool-midway/cache-manager-fs-hash": "^7.0.0",
|
||||||
|
"@midwayjs/cache": "^3.14.0",
|
||||||
|
"@midwayjs/cache-manager": "^3.15.0",
|
||||||
"axios": "^1.6.5",
|
"axios": "^1.6.5",
|
||||||
"decompress": "^4.2.1",
|
"decompress": "^4.2.1",
|
||||||
"download": "^8.0.0",
|
"download": "^8.0.0",
|
||||||
|
|||||||
@ -18,13 +18,13 @@ import { CoolModuleConfig } from "./module/config";
|
|||||||
import { CoolModuleImport } from "./module/import";
|
import { CoolModuleImport } from "./module/import";
|
||||||
import { CoolEventManager } from "./event";
|
import { CoolEventManager } from "./event";
|
||||||
import { CoolEps } from "./rest/eps";
|
import { CoolEps } from "./rest/eps";
|
||||||
import { CacheManager } from "@midwayjs/cache";
|
|
||||||
import * as cache from "@midwayjs/cache";
|
|
||||||
import { CoolDecorator } from "./decorator";
|
import { CoolDecorator } from "./decorator";
|
||||||
|
import * as cache from "@midwayjs/cache-manager";
|
||||||
|
import * as _cache from "@midwayjs/cache";
|
||||||
|
|
||||||
@Configuration({
|
@Configuration({
|
||||||
namespace: "cool",
|
namespace: "cool",
|
||||||
imports: [cache],
|
imports: [_cache, cache],
|
||||||
importConfigs: [
|
importConfigs: [
|
||||||
{
|
{
|
||||||
default: DefaultConfig,
|
default: DefaultConfig,
|
||||||
@ -59,7 +59,7 @@ export class CoolConfiguration implements ILifeCycle {
|
|||||||
const eps: CoolEps = await container.getAsync(CoolEps);
|
const eps: CoolEps = await container.getAsync(CoolEps);
|
||||||
eps.init();
|
eps.init();
|
||||||
// 缓存设置为全局
|
// 缓存设置为全局
|
||||||
global["COOL-CACHE"] = await container.getAsync(CacheManager);
|
// global["COOL-CACHE"] = await container.getAsync(CacheManager);
|
||||||
// 清除 location
|
// 清除 location
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
location.clean();
|
location.clean();
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { COOL_CACHE } from "./cache";
|
import { COOL_CACHE } from "./cache";
|
||||||
import { CacheManager } from "@midwayjs/cache";
|
import { CachingFactory, MidwayCache } from "@midwayjs/cache-manager";
|
||||||
import {
|
import {
|
||||||
Init,
|
Init,
|
||||||
Inject,
|
Inject,
|
||||||
|
InjectClient,
|
||||||
JoinPoint,
|
JoinPoint,
|
||||||
MidwayDecoratorService,
|
MidwayDecoratorService,
|
||||||
Provide,
|
Provide,
|
||||||
@ -27,8 +28,8 @@ export class CoolDecorator {
|
|||||||
@Inject()
|
@Inject()
|
||||||
decoratorService: MidwayDecoratorService;
|
decoratorService: MidwayDecoratorService;
|
||||||
|
|
||||||
@Inject()
|
@InjectClient(CachingFactory, "default")
|
||||||
cacheManager: CacheManager;
|
midwayCache: MidwayCache;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
coolUrlTagData: CoolUrlTagData;
|
coolUrlTagData: CoolUrlTagData;
|
||||||
@ -56,15 +57,17 @@ export class CoolDecorator {
|
|||||||
JSON.stringify(joinPoint.args)
|
JSON.stringify(joinPoint.args)
|
||||||
);
|
);
|
||||||
// 缓存有数据就返回
|
// 缓存有数据就返回
|
||||||
let data: any = await this.cacheManager.get(key);
|
let data: any = await this.midwayCache.get(key);
|
||||||
if (data) {
|
if (data) {
|
||||||
return JSON.parse(data);
|
return JSON.parse(data);
|
||||||
} else {
|
} else {
|
||||||
// 执行原始方法
|
// 执行原始方法
|
||||||
data = await joinPoint.proceed(...joinPoint.args);
|
data = await joinPoint.proceed(...joinPoint.args);
|
||||||
await this.cacheManager.set(key, JSON.stringify(data), {
|
await this.midwayCache.set(
|
||||||
ttl: options.metadata,
|
key,
|
||||||
});
|
JSON.stringify(data),
|
||||||
|
options.metadata
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|||||||
@ -7,6 +7,9 @@ export * from "./exception/base";
|
|||||||
export * from "./exception/comm";
|
export * from "./exception/comm";
|
||||||
export * from "./exception/validate";
|
export * from "./exception/validate";
|
||||||
|
|
||||||
|
// cache
|
||||||
|
export * from "./cache/store";
|
||||||
|
|
||||||
// entity
|
// entity
|
||||||
export * from "./entity/base";
|
export * from "./entity/base";
|
||||||
export * from "./entity/typeorm";
|
export * from "./entity/typeorm";
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@cool-midway/core",
|
"name": "@cool-midway/core",
|
||||||
"version": "7.1.4",
|
"version": "7.1.6",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"typings": "index.d.ts",
|
"typings": "index.d.ts",
|
||||||
@ -44,7 +44,9 @@
|
|||||||
"typescript": "~4.9.4"
|
"typescript": "~4.9.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@midwayjs/cache": "^3.9.0",
|
"@midwayjs/cache": "^3.14.0",
|
||||||
|
"@midwayjs/cache-manager": "^3.15.0",
|
||||||
|
"@cool-midway/cache-manager-fs-hash": "^7.0.0",
|
||||||
"axios": "^1.6.5",
|
"axios": "^1.6.5",
|
||||||
"decompress": "^4.2.1",
|
"decompress": "^4.2.1",
|
||||||
"download": "^8.0.0",
|
"download": "^8.0.0",
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
const fs = require('fs');
|
const fs = require("fs");
|
||||||
const crypto = require('crypto');
|
const crypto = require("crypto");
|
||||||
const path = require('path');
|
const path = require("path");
|
||||||
const promisify = require('util').promisify;
|
const promisify = require("util").promisify;
|
||||||
const lockFile = require('lockfile');
|
const lockFile = require("lockfile");
|
||||||
const jsonFileStore = require('./json-file-store');
|
const jsonFileStore = require("./json-file-store");
|
||||||
const wrapCallback = require('./wrap-callback');
|
const wrapCallback = require("./wrap-callback");
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* construction of the disk storage
|
* construction of the disk storage
|
||||||
@ -18,31 +17,32 @@ const wrapCallback = require('./wrap-callback');
|
|||||||
* @returns {DiskStore}
|
* @returns {DiskStore}
|
||||||
*/
|
*/
|
||||||
exports.create = function (args) {
|
exports.create = function (args) {
|
||||||
return new DiskStore(args && args.options ? args.options : args);
|
return new DiskStore(args && args.options ? args.options : args);
|
||||||
};
|
};
|
||||||
|
|
||||||
function DiskStore(options) {
|
function DiskStore(options) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
|
|
||||||
this.options = {
|
this.options = {
|
||||||
path: options.path || './cache', /* path for cached files */
|
path: options.path || "./cache" /* path for cached files */,
|
||||||
ttl: options.ttl, /* time before expiring in seconds */
|
ttl: options.ttl /* time before expiring in seconds */,
|
||||||
maxsize: options.maxsize || Infinity, /* max size in bytes on disk */
|
maxsize: options.maxsize || Infinity /* max size in bytes on disk */,
|
||||||
subdirs: options.subdirs || false,
|
subdirs: options.subdirs || false,
|
||||||
zip: options.zip || false,
|
zip: options.zip || false,
|
||||||
lockFile: { //check lock at 0ms 50ms 100ms ... 400ms 1400ms 1450ms... up to 10 seconds, after that just asume the lock is staled
|
lockFile: {
|
||||||
wait: 400,
|
//check lock at 0ms 50ms 100ms ... 400ms 1400ms 1450ms... up to 10 seconds, after that just asume the lock is staled
|
||||||
pollPeriod: 50,
|
wait: 400,
|
||||||
stale: 10 * 1000,
|
pollPeriod: 50,
|
||||||
retries: 10,
|
stale: 10 * 1000,
|
||||||
retryWait: 600,
|
retries: 10,
|
||||||
}
|
retryWait: 600,
|
||||||
};
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// check storage directory for existence (or create it)
|
// check storage directory for existence (or create it)
|
||||||
if (!fs.existsSync(this.options.path)) {
|
if (!fs.existsSync(this.options.path)) {
|
||||||
fs.mkdirSync(this.options.path);
|
fs.mkdirSync(this.options.path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -55,78 +55,77 @@ function DiskStore(options) {
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
DiskStore.prototype.set = wrapCallback(async function (key, val, options) {
|
DiskStore.prototype.set = wrapCallback(async function (key, val, options) {
|
||||||
key = key + '';
|
key = key + "";
|
||||||
const filePath = this._getFilePathByKey(key);
|
const filePath = this._getFilePathByKey(key);
|
||||||
|
|
||||||
const ttl = (options && (options.ttl >= 0)) ? +options.ttl : this.options.ttl;
|
const ttl = options && options.ttl >= 0 ? +options.ttl : this.options.ttl;
|
||||||
const data = {
|
const data = {
|
||||||
key: key,
|
key: key,
|
||||||
val: val,
|
val: val,
|
||||||
};
|
};
|
||||||
if(ttl>0){
|
if (ttl > 0) {
|
||||||
data.expireTime = Date.now() + ttl * 1000;
|
data.expireTime = Date.now() + ttl * 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.options.subdirs) {
|
||||||
|
//check if subdir exists or create it
|
||||||
|
const dir = path.dirname(filePath);
|
||||||
|
await promisify(fs.access)(dir, fs.constants.W_OK).catch(function () {
|
||||||
|
return promisify(fs.mkdir)(dir).catch((err) => {
|
||||||
|
if (err.code !== "EEXIST") throw err;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (this.options.subdirs) {
|
try {
|
||||||
//check if subdir exists or create it
|
await this._lock(filePath);
|
||||||
const dir = path.dirname(filePath);
|
await jsonFileStore.write(filePath, data, this.options);
|
||||||
await promisify(fs.access)(dir, fs.constants.W_OK).catch(function () {
|
} catch (err) {
|
||||||
return promisify(fs.mkdir)(dir).catch(err => {
|
throw err;
|
||||||
if (err.code !== 'EEXIST') throw err;
|
} finally {
|
||||||
});
|
await this._unlock(filePath);
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this._lock(filePath);
|
|
||||||
await jsonFileStore.write(filePath, data, this.options);
|
|
||||||
} catch (err) {
|
|
||||||
throw err;
|
|
||||||
} finally {
|
|
||||||
await this._unlock(filePath);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
DiskStore.prototype._readFile = async function (key) {
|
DiskStore.prototype._readFile = async function (key) {
|
||||||
key = key + '';
|
key = key + "";
|
||||||
const filePath = this._getFilePathByKey(key);
|
const filePath = this._getFilePathByKey(key);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await jsonFileStore.read(filePath, this.options).catch(async (err) => {
|
const data = await jsonFileStore
|
||||||
if (err.code === 'ENOENT') {
|
.read(filePath, this.options)
|
||||||
throw err;
|
.catch(async (err) => {
|
||||||
}
|
if (err.code === "ENOENT") {
|
||||||
//maybe the file is currently written to, lets lock it and read again
|
throw err;
|
||||||
try {
|
|
||||||
await this._lock(filePath);
|
|
||||||
return await jsonFileStore.read(filePath, this.options);
|
|
||||||
} catch (err2) {
|
|
||||||
throw err2;
|
|
||||||
} finally {
|
|
||||||
await this._unlock(filePath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (data.expireTime <= Date.now()) {
|
|
||||||
//cache expired
|
|
||||||
this.del(key).catch(() => 0 /* ignore */);
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
if (data.key !== key) {
|
//maybe the file is currently written to, lets lock it and read again
|
||||||
//hash collision
|
try {
|
||||||
return undefined;
|
await this._lock(filePath);
|
||||||
}
|
return await jsonFileStore.read(filePath, this.options);
|
||||||
return data;
|
} catch (err2) {
|
||||||
|
throw err2;
|
||||||
} catch (err) {
|
} finally {
|
||||||
//file does not exist lets return a cache miss
|
await this._unlock(filePath);
|
||||||
if (err.code === 'ENOENT') {
|
|
||||||
return undefined;
|
|
||||||
} else {
|
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
if (data.expireTime <= Date.now()) {
|
||||||
|
//cache expired
|
||||||
|
this.del(key).catch(() => 0 /* ignore */);
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
if (data.key !== key) {
|
||||||
|
//hash collision
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
//file does not exist lets return a cache miss
|
||||||
|
if (err.code === "ENOENT") {
|
||||||
|
return undefined;
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -136,12 +135,12 @@ DiskStore.prototype._readFile = async function (key) {
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
DiskStore.prototype.get = wrapCallback(async function (key) {
|
DiskStore.prototype.get = wrapCallback(async function (key) {
|
||||||
const data = await this._readFile(key);
|
const data = await this._readFile(key);
|
||||||
if (data) {
|
if (data) {
|
||||||
return data.val;
|
return data.val;
|
||||||
} else {
|
} else {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -151,68 +150,68 @@ DiskStore.prototype.get = wrapCallback(async function (key) {
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
DiskStore.prototype.ttl = wrapCallback(async function (key) {
|
DiskStore.prototype.ttl = wrapCallback(async function (key) {
|
||||||
const data = await this._readFile(key);
|
const data = await this._readFile(key);
|
||||||
if (data) {
|
if (data) {
|
||||||
return (data.expireTime - Date.now()) / 1000;
|
return (data.expireTime - Date.now()) / 1000;
|
||||||
} else {
|
} else {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* delete entry from cache
|
* delete entry from cache
|
||||||
*/
|
*/
|
||||||
DiskStore.prototype.del = wrapCallback(async function (key) {
|
DiskStore.prototype.del = wrapCallback(async function (key) {
|
||||||
const filePath = this._getFilePathByKey(key);
|
const filePath = this._getFilePathByKey(key);
|
||||||
try {
|
try {
|
||||||
if (this.options.subdirs) {
|
if (this.options.subdirs) {
|
||||||
//check if the folder exists to fail faster
|
//check if the folder exists to fail faster
|
||||||
const dir = path.dirname(filePath);
|
const dir = path.dirname(filePath);
|
||||||
await promisify(fs.access)(dir, fs.constants.W_OK);
|
await promisify(fs.access)(dir, fs.constants.W_OK);
|
||||||
}
|
|
||||||
|
|
||||||
await this._lock(filePath);
|
|
||||||
await jsonFileStore.delete(filePath, this.options);
|
|
||||||
} catch (err) {
|
|
||||||
//ignore deleting non existing keys
|
|
||||||
if (err.code !== 'ENOENT') {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
await this._unlock(filePath);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
|
await this._lock(filePath);
|
||||||
|
await jsonFileStore.delete(filePath, this.options);
|
||||||
|
} catch (err) {
|
||||||
|
//ignore deleting non existing keys
|
||||||
|
if (err.code !== "ENOENT") {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await this._unlock(filePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* cleanup cache on disk -> delete all files from the cache
|
* cleanup cache on disk -> delete all files from the cache
|
||||||
*/
|
*/
|
||||||
DiskStore.prototype.reset = wrapCallback(async function () {
|
DiskStore.prototype.reset = wrapCallback(async function () {
|
||||||
const readdir = promisify(fs.readdir);
|
const readdir = promisify(fs.readdir);
|
||||||
const stat = promisify(fs.stat);
|
const stat = promisify(fs.stat);
|
||||||
const unlink = promisify(fs.unlink);
|
const unlink = promisify(fs.unlink);
|
||||||
|
|
||||||
return await deletePath(this.options.path, 2);
|
return await deletePath(this.options.path, 2);
|
||||||
|
|
||||||
async function deletePath(fileOrDir, maxDeep) {
|
async function deletePath(fileOrDir, maxDeep) {
|
||||||
if (maxDeep < 0) {
|
if (maxDeep < 0) {
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
const stats = await stat(fileOrDir);
|
|
||||||
if (stats.isDirectory()) {
|
|
||||||
const files = await readdir(fileOrDir);
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
|
||||||
await deletePath(path.join(fileOrDir, files[i]), maxDeep - 1);
|
|
||||||
}
|
|
||||||
} else if (stats.isFile() && /[/\\]diskstore-[0-9a-fA-F/\\]+(\.json|-\d\.bin)/.test(fileOrDir)) {
|
|
||||||
//delete the file if it is a diskstore file
|
|
||||||
await unlink(fileOrDir);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
const stats = await stat(fileOrDir);
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
const files = await readdir(fileOrDir);
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
await deletePath(path.join(fileOrDir, files[i]), maxDeep - 1);
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
stats.isFile() &&
|
||||||
|
/[/\\]diskstore-[0-9a-fA-F/\\]+(\.json|-\d\.bin)/.test(fileOrDir)
|
||||||
|
) {
|
||||||
|
//delete the file if it is a diskstore file
|
||||||
|
await unlink(fileOrDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* locks a file so other forks that want to use the same file have to wait
|
* locks a file so other forks that want to use the same file have to wait
|
||||||
* @param {string} filePath
|
* @param {string} filePath
|
||||||
@ -220,10 +219,10 @@ DiskStore.prototype.reset = wrapCallback(async function () {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
DiskStore.prototype._lock = function (filePath) {
|
DiskStore.prototype._lock = function (filePath) {
|
||||||
return promisify(lockFile.lock)(
|
return promisify(lockFile.lock)(
|
||||||
filePath + '.lock',
|
filePath + ".lock",
|
||||||
JSON.parse(JSON.stringify(this.options.lockFile)) //the options are modified -> create a copy to prevent that
|
JSON.parse(JSON.stringify(this.options.lockFile)) //the options are modified -> create a copy to prevent that
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -234,7 +233,7 @@ DiskStore.prototype._lock = function (filePath) {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
DiskStore.prototype._unlock = function (filePath) {
|
DiskStore.prototype._unlock = function (filePath) {
|
||||||
return promisify(lockFile.unlock)(filePath + '.lock');
|
return promisify(lockFile.unlock)(filePath + ".lock");
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -244,18 +243,18 @@ DiskStore.prototype._unlock = function (filePath) {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
DiskStore.prototype._getFilePathByKey = function (key) {
|
DiskStore.prototype._getFilePathByKey = function (key) {
|
||||||
const hash = crypto.createHash('md5').update(key + '').digest('hex');
|
const hash = crypto
|
||||||
if (this.options.subdirs) {
|
.createHash("md5")
|
||||||
//create subdirs with the first 3 chars of the hash
|
.update(key + "")
|
||||||
return path.join(
|
.digest("hex");
|
||||||
this.options.path,
|
if (this.options.subdirs) {
|
||||||
'diskstore-' + hash.substr(0, 3),
|
//create subdirs with the first 3 chars of the hash
|
||||||
hash.substr(3),
|
return path.join(
|
||||||
);
|
this.options.path,
|
||||||
} else {
|
"diskstore-" + hash.substr(0, 3),
|
||||||
return path.join(
|
hash.substr(3)
|
||||||
this.options.path,
|
);
|
||||||
'diskstore-' + hash
|
} else {
|
||||||
);
|
return path.join(this.options.path, "diskstore-" + hash);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user