[refactor] Expose all store prototypes on nconf.*
. Expose store instances on Provider.stores and Provider.sources
This commit is contained in:
parent
c3cebe7cb4
commit
16a18bffe6
12 changed files with 90 additions and 102 deletions
13
lib/nconf.js
13
lib/nconf.js
|
@ -16,6 +16,18 @@ var fs = require('fs'),
|
||||||
//
|
//
|
||||||
require('pkginfo')(module, 'version');
|
require('pkginfo')(module, 'version');
|
||||||
|
|
||||||
|
//
|
||||||
|
// Setup all stores as lazy-loaded getters.
|
||||||
|
//
|
||||||
|
fs.readdirSync(__dirname + '/nconf/stores').forEach(function (file) {
|
||||||
|
var store = file.replace('.js', ''),
|
||||||
|
name = common.capitalize(store);
|
||||||
|
|
||||||
|
nconf.__defineGetter__(name, function () {
|
||||||
|
return require('./nconf/stores/' + store)[name];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
//
|
//
|
||||||
// Expose the various components included with nconf
|
// Expose the various components included with nconf
|
||||||
//
|
//
|
||||||
|
@ -24,5 +36,4 @@ nconf.path = common.path;
|
||||||
nconf.loadFiles = common.loadFiles;
|
nconf.loadFiles = common.loadFiles;
|
||||||
nconf.loadFilesSync = common.loadFilesSync;
|
nconf.loadFilesSync = common.loadFilesSync;
|
||||||
nconf.formats = require('./nconf/formats');
|
nconf.formats = require('./nconf/formats');
|
||||||
nconf.stores = require('./nconf/stores');
|
|
||||||
nconf.Provider = Provider;
|
nconf.Provider = Provider;
|
|
@ -6,10 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var async = require('async'),
|
var async = require('async'),
|
||||||
common = require('./common'),
|
common = require('./common');
|
||||||
stores = require('./stores');
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// ### function Provider (options)
|
// ### function Provider (options)
|
||||||
|
@ -25,38 +22,9 @@ var Provider = exports.Provider = function (options) {
|
||||||
// `overrides`, `process.env` and `process.argv`.
|
// `overrides`, `process.env` and `process.argv`.
|
||||||
//
|
//
|
||||||
options = options || {};
|
options = options || {};
|
||||||
this._stores = {};
|
this.stores = {};
|
||||||
this._sources = [];
|
this.sources = [];
|
||||||
|
this.init(options);
|
||||||
//
|
|
||||||
// Add any stores passed in through the options
|
|
||||||
// to this instance.
|
|
||||||
//
|
|
||||||
if (options.type) {
|
|
||||||
this.add(options.type, options);
|
|
||||||
}
|
|
||||||
else if (options.store) {
|
|
||||||
this.add(options.store.name || options.store.type, options.store);
|
|
||||||
}
|
|
||||||
else if (options.stores) {
|
|
||||||
Object.keys(options.stores).forEach(function (name) {
|
|
||||||
var store = options.stores[name];
|
|
||||||
self.add(store.name || name || store.type, store);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Add any read-only sources to this instance
|
|
||||||
//
|
|
||||||
if (options.source) {
|
|
||||||
this._sources.push(this.create(options.source.type || options.source.name, options.source));
|
|
||||||
}
|
|
||||||
else if (options.sources) {
|
|
||||||
Object.keys(options.sources).forEach(function (name) {
|
|
||||||
var source = options.sources[name];
|
|
||||||
self._sources.push(self.create(source.type || source.name || name, source));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -100,7 +68,7 @@ Provider.prototype.use = function (name, options) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var store = this._stores[name],
|
var store = this.stores[name],
|
||||||
update = store && !sameOptions(store);
|
update = store && !sameOptions(store);
|
||||||
|
|
||||||
if (!store || update) {
|
if (!store || update) {
|
||||||
|
@ -128,14 +96,14 @@ Provider.prototype.add = function (name, options) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
var type = options.type || name;
|
var type = options.type || name;
|
||||||
|
|
||||||
if (Object.keys(stores).indexOf(common.capitalize(type)) === -1) {
|
if (!require('../nconf')[common.capitalize(type)]) {
|
||||||
throw new Error('Cannot add store with unknown type: ' + type);
|
throw new Error('Cannot add store with unknown type: ' + type);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._stores[name] = this.create(type, options);
|
this.stores[name] = this.create(type, options);
|
||||||
|
|
||||||
if (this._stores[name].loadSync) {
|
if (this.stores[name].loadSync) {
|
||||||
this._stores[name].loadSync();
|
this.stores[name].loadSync();
|
||||||
}
|
}
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
|
@ -149,7 +117,7 @@ Provider.prototype.add = function (name, options) {
|
||||||
// this was used in the call to `.add()`.
|
// this was used in the call to `.add()`.
|
||||||
//
|
//
|
||||||
Provider.prototype.remove = function (name) {
|
Provider.prototype.remove = function (name) {
|
||||||
delete this._stores[name];
|
delete this.stores[name];
|
||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -161,7 +129,39 @@ Provider.prototype.remove = function (name) {
|
||||||
// specified `options`.
|
// specified `options`.
|
||||||
//
|
//
|
||||||
Provider.prototype.create = function (type, options) {
|
Provider.prototype.create = function (type, options) {
|
||||||
return new stores[common.capitalize(type.toLowerCase())](options);
|
return new (require('../nconf')[common.capitalize(type.toLowerCase())])(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
Provider.prototype.init = function (options) {
|
||||||
|
//
|
||||||
|
// Add any stores passed in through the options
|
||||||
|
// to this instance.
|
||||||
|
//
|
||||||
|
if (options.type) {
|
||||||
|
this.add(options.type, options);
|
||||||
|
}
|
||||||
|
else if (options.store) {
|
||||||
|
this.add(options.store.name || options.store.type, options.store);
|
||||||
|
}
|
||||||
|
else if (options.stores) {
|
||||||
|
Object.keys(options.stores).forEach(function (name) {
|
||||||
|
var store = options.stores[name];
|
||||||
|
self.add(store.name || name || store.type, store);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Add any read-only sources to this instance
|
||||||
|
//
|
||||||
|
if (options.source) {
|
||||||
|
this.sources.push(this.create(options.source.type || options.source.name, options.source));
|
||||||
|
}
|
||||||
|
else if (options.sources) {
|
||||||
|
Object.keys(options.sources).forEach(function (name) {
|
||||||
|
var source = options.sources[name];
|
||||||
|
self.sources.push(self.create(source.type || source.name || name, source));
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -185,14 +185,14 @@ Provider.prototype.get = function (key, callback) {
|
||||||
// the entire set of stores, but up until there is a defined value.
|
// the entire set of stores, but up until there is a defined value.
|
||||||
//
|
//
|
||||||
var current = 0,
|
var current = 0,
|
||||||
names = Object.keys(this._stores),
|
names = Object.keys(this.stores),
|
||||||
self = this,
|
self = this,
|
||||||
response;
|
response;
|
||||||
|
|
||||||
async.whilst(function () {
|
async.whilst(function () {
|
||||||
return typeof response === 'undefined' && current < names.length;
|
return typeof response === 'undefined' && current < names.length;
|
||||||
}, function (next) {
|
}, function (next) {
|
||||||
var store = self._stores[names[current]];
|
var store = self.stores[names[current]];
|
||||||
current++;
|
current++;
|
||||||
|
|
||||||
if (store.get.length >= 2) {
|
if (store.get.length >= 2) {
|
||||||
|
@ -282,7 +282,7 @@ Provider.prototype.merge = function () {
|
||||||
//
|
//
|
||||||
Provider.prototype.load = function (callback) {
|
Provider.prototype.load = function (callback) {
|
||||||
var self = this,
|
var self = this,
|
||||||
stores = Object.keys(this._stores).map(function (name) { return self._stores[name] });
|
stores = Object.keys(this.stores).map(function (name) { return self.stores[name] });
|
||||||
|
|
||||||
function loadStoreSync(store) {
|
function loadStoreSync(store) {
|
||||||
if (!store.loadSync) {
|
if (!store.loadSync) {
|
||||||
|
@ -331,11 +331,11 @@ Provider.prototype.load = function (callback) {
|
||||||
// then do so.
|
// then do so.
|
||||||
//
|
//
|
||||||
if (!callback) {
|
if (!callback) {
|
||||||
mergeSources(loadBatch(self._sources));
|
mergeSources(loadBatch(self.sources));
|
||||||
return loadBatch(stores);
|
return loadBatch(stores);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadBatch(self._sources, function (err, data) {
|
loadBatch(self.sources, function (err, data) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
@ -345,7 +345,7 @@ Provider.prototype.load = function (callback) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return self._sources.length
|
return self.sources.length
|
||||||
? loadSources()
|
? loadSources()
|
||||||
: loadBatch(stores, callback);
|
: loadBatch(stores, callback);
|
||||||
};
|
};
|
||||||
|
@ -364,10 +364,10 @@ Provider.prototype.save = function (value, callback) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var self = this,
|
var self = this,
|
||||||
names = Object.keys(this._stores);
|
names = Object.keys(this.stores);
|
||||||
|
|
||||||
function saveStoreSync(name) {
|
function saveStoreSync(name) {
|
||||||
var store = self._stores[name];
|
var store = self.stores[name];
|
||||||
|
|
||||||
if (!store.saveSync) {
|
if (!store.saveSync) {
|
||||||
throw new Error('nconf store ' + store.type + ' has no saveSync() method');
|
throw new Error('nconf store ' + store.type + ' has no saveSync() method');
|
||||||
|
@ -377,7 +377,7 @@ Provider.prototype.save = function (value, callback) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveStore(name, next) {
|
function saveStore(name, next) {
|
||||||
var store = self._stores[name];
|
var store = self.stores[name];
|
||||||
|
|
||||||
if (!store.save && !store.saveSync) {
|
if (!store.save && !store.saveSync) {
|
||||||
return next(new Error('nconf store ' + store.type + ' has no save() method'));
|
return next(new Error('nconf store ' + store.type + ' has no save() method'));
|
||||||
|
@ -418,7 +418,7 @@ Provider.prototype._execute = function (action, syncLength /* [arguments] */) {
|
||||||
response;
|
response;
|
||||||
|
|
||||||
function runAction (name, next) {
|
function runAction (name, next) {
|
||||||
var store = self._stores[name];
|
var store = self.stores[name];
|
||||||
|
|
||||||
if (destructive && store.readOnly) {
|
if (destructive && store.readOnly) {
|
||||||
return next();
|
return next();
|
||||||
|
@ -430,15 +430,15 @@ Provider.prototype._execute = function (action, syncLength /* [arguments] */) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (callback) {
|
if (callback) {
|
||||||
return async.forEach(Object.keys(this._stores), runAction, function (err) {
|
return async.forEach(Object.keys(this.stores), runAction, function (err) {
|
||||||
return err ? callback(err) : callback();
|
return err ? callback(err) : callback();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Object.keys(this._stores).forEach(function (name) {
|
Object.keys(this.stores).forEach(function (name) {
|
||||||
if (typeof response === 'undefined') {
|
if (typeof response === 'undefined') {
|
||||||
var store = self._stores[name];
|
var store = self.stores[name];
|
||||||
|
|
||||||
if (destructive && store.readOnly) {
|
if (destructive && store.readOnly) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
/*
|
|
||||||
* stores.js: Top-level include for all nconf stores
|
|
||||||
*
|
|
||||||
* (C) 2011, Charlie Robbins
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
var fs = require('fs'),
|
|
||||||
common = require('./common'),
|
|
||||||
stores = exports;
|
|
||||||
|
|
||||||
//
|
|
||||||
// Setup all stores as lazy-loaded getters.
|
|
||||||
//
|
|
||||||
fs.readdirSync(__dirname + '/stores').forEach(function (file) {
|
|
||||||
var store = file.replace('.js', ''),
|
|
||||||
name = common.capitalize(store);
|
|
||||||
|
|
||||||
stores.__defineGetter__(name, function () {
|
|
||||||
return require('./stores/' + store)[name];
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -6,7 +6,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var util = require('util'),
|
var util = require('util'),
|
||||||
optimist = require('optimist'),
|
|
||||||
Memory = require('./memory').Memory;
|
Memory = require('./memory').Memory;
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -45,8 +44,8 @@ Argv.prototype.loadArgv = function () {
|
||||||
argv;
|
argv;
|
||||||
|
|
||||||
argv = typeof this.options === 'object'
|
argv = typeof this.options === 'object'
|
||||||
? optimist(process.argv.slice(2)).options(this.options).argv
|
? require('optimist')(process.argv.slice(2)).options(this.options).argv
|
||||||
: optimist(process.argv.slice(2)).argv;
|
: require('optimist')(process.argv.slice(2)).argv;
|
||||||
|
|
||||||
if (!argv) {
|
if (!argv) {
|
||||||
return;
|
return;
|
||||||
|
|
2
test/fixtures/scripts/nconf-change-argv.js
vendored
2
test/fixtures/scripts/nconf-change-argv.js
vendored
|
@ -11,6 +11,6 @@ var nconf = require('../../../lib/nconf').argv();
|
||||||
// Remove 'badValue', 'evenWorse' and 'OHNOEZ'
|
// Remove 'badValue', 'evenWorse' and 'OHNOEZ'
|
||||||
//
|
//
|
||||||
process.argv.splice(3, 3);
|
process.argv.splice(3, 3);
|
||||||
nconf._stores['argv'].loadArgv();
|
nconf.stores['argv'].loadArgv();
|
||||||
process.stdout.write(nconf.get('something'));
|
process.stdout.write(nconf.get('something'));
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ vows.describe('nconf').addBatch({
|
||||||
"the use() method": {
|
"the use() method": {
|
||||||
"should instaniate the correct store": function () {
|
"should instaniate the correct store": function () {
|
||||||
nconf.use('memory');
|
nconf.use('memory');
|
||||||
assert.instanceOf(nconf._stores['memory'], nconf.stores.Memory);
|
assert.instanceOf(nconf.stores['memory'], nconf.Memory);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"it should": {
|
"it should": {
|
||||||
|
|
|
@ -24,13 +24,13 @@ vows.describe('nconf/provider').addBatch({
|
||||||
"calling the use() method with the same store type and different options": {
|
"calling the use() method with the same store type and different options": {
|
||||||
topic: new nconf.Provider().use('file', { file: files[0] }),
|
topic: new nconf.Provider().use('file', { file: files[0] }),
|
||||||
"should use a new instance of the store type": function (provider) {
|
"should use a new instance of the store type": function (provider) {
|
||||||
var old = provider._stores['file'];
|
var old = provider.stores['file'];
|
||||||
|
|
||||||
assert.equal(provider._stores.file.file, files[0]);
|
assert.equal(provider.stores.file.file, files[0]);
|
||||||
provider.use('file', { file: files[1] });
|
provider.use('file', { file: files[1] });
|
||||||
|
|
||||||
assert.notStrictEqual(old, provider._stores.file);
|
assert.notStrictEqual(old, provider.stores.file);
|
||||||
assert.equal(provider._stores.file.file, files[1]);
|
assert.equal(provider.stores.file.file, files[1]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"when 'argv' is true": helpers.assertSystemConf({
|
"when 'argv' is true": helpers.assertSystemConf({
|
||||||
|
@ -71,7 +71,7 @@ vows.describe('nconf/provider').addBatch({
|
||||||
"should have the result merged in": function (provider) {
|
"should have the result merged in": function (provider) {
|
||||||
provider.load();
|
provider.load();
|
||||||
provider.merge(override);
|
provider.merge(override);
|
||||||
helpers.assertMerged(null, provider._stores.file.store);
|
helpers.assertMerged(null, provider.stores.file.store);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,8 +11,8 @@ var vows = require('vows'),
|
||||||
nconf = require('../../lib/nconf');
|
nconf = require('../../lib/nconf');
|
||||||
|
|
||||||
vows.describe('nconf/stores/argv').addBatch({
|
vows.describe('nconf/stores/argv').addBatch({
|
||||||
"An instance of nconf.stores.Argv": {
|
"An instance of nconf.Argv": {
|
||||||
topic: new nconf.stores.Argv(),
|
topic: new nconf.Argv(),
|
||||||
"should have the correct methods defined": function (argv) {
|
"should have the correct methods defined": function (argv) {
|
||||||
assert.isFunction(argv.loadSync);
|
assert.isFunction(argv.loadSync);
|
||||||
assert.isFunction(argv.loadArgv);
|
assert.isFunction(argv.loadArgv);
|
||||||
|
|
|
@ -11,8 +11,8 @@ var vows = require('vows'),
|
||||||
nconf = require('../../lib/nconf');
|
nconf = require('../../lib/nconf');
|
||||||
|
|
||||||
vows.describe('nconf/stores/env').addBatch({
|
vows.describe('nconf/stores/env').addBatch({
|
||||||
"An instance of nconf.stores.Env": {
|
"An instance of nconf.Env": {
|
||||||
topic: new nconf.stores.Env(),
|
topic: new nconf.Env(),
|
||||||
"should have the correct methods defined": function (env) {
|
"should have the correct methods defined": function (env) {
|
||||||
assert.isFunction(env.loadSync);
|
assert.isFunction(env.loadSync);
|
||||||
assert.isFunction(env.loadEnv);
|
assert.isFunction(env.loadEnv);
|
||||||
|
|
|
@ -18,7 +18,7 @@ vows.describe('nconf/stores/file').addBatch({
|
||||||
topic: function () {
|
topic: function () {
|
||||||
var filePath = path.join(__dirname, '..', 'fixtures', 'store.json');
|
var filePath = path.join(__dirname, '..', 'fixtures', 'store.json');
|
||||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
||||||
store = new nconf.stores.File({ file: filePath });
|
store = new nconf.File({ file: filePath });
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
"the load() method": {
|
"the load() method": {
|
||||||
|
@ -34,7 +34,7 @@ vows.describe('nconf/stores/file').addBatch({
|
||||||
"When using the nconf file store": {
|
"When using the nconf file store": {
|
||||||
topic: function () {
|
topic: function () {
|
||||||
var filePath = path.join(__dirname, '..', 'fixtures', 'malformed.json');
|
var filePath = path.join(__dirname, '..', 'fixtures', 'malformed.json');
|
||||||
store = new nconf.stores.File({ file: filePath });
|
store = new nconf.File({ file: filePath });
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
"the load() method with a malformed JSON config file": {
|
"the load() method with a malformed JSON config file": {
|
||||||
|
@ -50,7 +50,7 @@ vows.describe('nconf/stores/file').addBatch({
|
||||||
"When using the nconf file store": {
|
"When using the nconf file store": {
|
||||||
topic: function () {
|
topic: function () {
|
||||||
var tmpPath = path.join(__dirname, '..', 'fixtures', 'tmp.json'),
|
var tmpPath = path.join(__dirname, '..', 'fixtures', 'tmp.json'),
|
||||||
tmpStore = new nconf.stores.File({ file: tmpPath });
|
tmpStore = new nconf.File({ file: tmpPath });
|
||||||
return tmpStore;
|
return tmpStore;
|
||||||
},
|
},
|
||||||
"the save() method": {
|
"the save() method": {
|
||||||
|
@ -108,7 +108,7 @@ vows.describe('nconf/stores/file').addBatch({
|
||||||
topic: function () {
|
topic: function () {
|
||||||
var filePath = this.filePath = path.join(process.env.HOME, '.nconf');
|
var filePath = this.filePath = path.join(process.env.HOME, '.nconf');
|
||||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
||||||
return new (nconf.stores.File)({
|
return new (nconf.File)({
|
||||||
file: '.nconf'
|
file: '.nconf'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -121,7 +121,7 @@ vows.describe('nconf/stores/file').addBatch({
|
||||||
"when the target file doesn't exist higher in the directory tree": {
|
"when the target file doesn't exist higher in the directory tree": {
|
||||||
topic: function () {
|
topic: function () {
|
||||||
var filePath = this.filePath = path.join(__dirname, '..', 'fixtures', 'search-store.json');
|
var filePath = this.filePath = path.join(__dirname, '..', 'fixtures', 'search-store.json');
|
||||||
return new (nconf.stores.File)({
|
return new (nconf.File)({
|
||||||
dir: path.dirname(filePath),
|
dir: path.dirname(filePath),
|
||||||
file: 'search-store.json'
|
file: 'search-store.json'
|
||||||
})
|
})
|
||||||
|
|
|
@ -11,8 +11,8 @@ var vows = require('vows'),
|
||||||
nconf = require('../../lib/nconf');
|
nconf = require('../../lib/nconf');
|
||||||
|
|
||||||
vows.describe('nconf/stores/literal').addBatch({
|
vows.describe('nconf/stores/literal').addBatch({
|
||||||
"An instance of nconf.stores.Literal": {
|
"An instance of nconf.Literal": {
|
||||||
topic: new nconf.stores.Literal({
|
topic: new nconf.Literal({
|
||||||
foo: 'bar',
|
foo: 'bar',
|
||||||
one: 2
|
one: 2
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -12,7 +12,7 @@ var vows = require('vows'),
|
||||||
|
|
||||||
vows.describe('nconf/stores/memory').addBatch({
|
vows.describe('nconf/stores/memory').addBatch({
|
||||||
"When using the nconf memory store": {
|
"When using the nconf memory store": {
|
||||||
topic: new nconf.stores.Memory(),
|
topic: new nconf.Memory(),
|
||||||
"the set() method": {
|
"the set() method": {
|
||||||
"should respond with true": function (store) {
|
"should respond with true": function (store) {
|
||||||
assert.isTrue(store.set('foo:bar:bazz', 'buzz'));
|
assert.isTrue(store.set('foo:bar:bazz', 'buzz'));
|
||||||
|
|
Loading…
Reference in a new issue