mirror of
https://github.com/actions/cache.git
synced 2025-06-29 13:50:50 +02:00
Compare commits
1 Commits
v3.0.7
...
tiwarishub
Author | SHA1 | Date | |
---|---|---|---|
402b512df8 |
2
.licenses/npm/@actions/cache.dep.yml
generated
2
.licenses/npm/@actions/cache.dep.yml
generated
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: "@actions/cache"
|
name: "@actions/cache"
|
||||||
version: 3.0.3
|
version: 3.0.0
|
||||||
type: npm
|
type: npm
|
||||||
summary:
|
summary:
|
||||||
homepage:
|
homepage:
|
||||||
|
@ -15,9 +15,6 @@ See ["Caching dependencies to speed up workflows"](https://help.github.com/githu
|
|||||||
* Updated the minimum runner version support from node 12 -> node 16.
|
* Updated the minimum runner version support from node 12 -> node 16.
|
||||||
* Fixed avoiding empty cache save when no files are available for caching.
|
* Fixed avoiding empty cache save when no files are available for caching.
|
||||||
* Fixed tar creation error while trying to create tar with path as `~/` home folder on `ubuntu-latest`.
|
* Fixed tar creation error while trying to create tar with path as `~/` home folder on `ubuntu-latest`.
|
||||||
* Fixed zstd failing on amazon linux 2.0 runners
|
|
||||||
* Fixed cache not working with github workspace directory or current directory
|
|
||||||
* Fixed the download stuck problem by introducing a timeout of 1 hour for cache downloads.
|
|
||||||
|
|
||||||
Refer [here](https://github.com/actions/cache/blob/v2/README.md) for previous versions
|
Refer [here](https://github.com/actions/cache/blob/v2/README.md) for previous versions
|
||||||
|
|
||||||
@ -84,7 +81,6 @@ Every programming language and framework has its own way of caching.
|
|||||||
See [Examples](examples.md) for a list of `actions/cache` implementations for use with:
|
See [Examples](examples.md) for a list of `actions/cache` implementations for use with:
|
||||||
|
|
||||||
- [C# - NuGet](./examples.md#c---nuget)
|
- [C# - NuGet](./examples.md#c---nuget)
|
||||||
- [Clojure - Lein Deps](./examples.md#clojure---lein-deps)
|
|
||||||
- [D - DUB](./examples.md#d---dub)
|
- [D - DUB](./examples.md#d---dub)
|
||||||
- [Deno](./examples.md#deno)
|
- [Deno](./examples.md#deno)
|
||||||
- [Elixir - Mix](./examples.md#elixir---mix)
|
- [Elixir - Mix](./examples.md#elixir---mix)
|
||||||
|
@ -17,11 +17,4 @@
|
|||||||
- Fixed tar creation error while trying to create tar with path as `~/` home folder on `ubuntu-latest`. ([issue](https://github.com/actions/cache/issues/689))
|
- Fixed tar creation error while trying to create tar with path as `~/` home folder on `ubuntu-latest`. ([issue](https://github.com/actions/cache/issues/689))
|
||||||
|
|
||||||
### 3.0.5
|
### 3.0.5
|
||||||
- Removed error handling by consuming actions/cache 3.0 toolkit, Now cache server error handling will be done by toolkit. ([PR](https://github.com/actions/cache/pull/834))
|
- Removed error handling by consuming actions/cache 3.0 toolkit, Now cache server error handling will be done by toolkit. ([PR](https://github.com/actions/cache/pull/834))
|
||||||
|
|
||||||
### 3.0.6
|
|
||||||
- Fixed [#809](https://github.com/actions/cache/issues/809) - zstd -d: no such file or directory error
|
|
||||||
- Fixed [#833](https://github.com/actions/cache/issues/833) - cache doesn't work with github workspace directory
|
|
||||||
|
|
||||||
### 3.0.7
|
|
||||||
- Fixed [#810](https://github.com/actions/cache/issues/810) - download stuck issue. A new timeout is introduced in the download process to abort the download if it gets stuck and doesn't finish within an hour.
|
|
BIN
actions-cache-3.0.1.tgz
Normal file
BIN
actions-cache-3.0.1.tgz
Normal file
Binary file not shown.
45
dist/restore/index.js
vendored
45
dist/restore/index.js
vendored
@ -5470,10 +5470,10 @@ const buffer = __importStar(__webpack_require__(293));
|
|||||||
const fs = __importStar(__webpack_require__(747));
|
const fs = __importStar(__webpack_require__(747));
|
||||||
const stream = __importStar(__webpack_require__(794));
|
const stream = __importStar(__webpack_require__(794));
|
||||||
const util = __importStar(__webpack_require__(669));
|
const util = __importStar(__webpack_require__(669));
|
||||||
|
const timer = __importStar(__webpack_require__(581));
|
||||||
const utils = __importStar(__webpack_require__(15));
|
const utils = __importStar(__webpack_require__(15));
|
||||||
const constants_1 = __webpack_require__(931);
|
const constants_1 = __webpack_require__(931);
|
||||||
const requestUtils_1 = __webpack_require__(899);
|
const requestUtils_1 = __webpack_require__(899);
|
||||||
const abort_controller_1 = __webpack_require__(106);
|
|
||||||
/**
|
/**
|
||||||
* Pipes the body of a HTTP response to a stream
|
* Pipes the body of a HTTP response to a stream
|
||||||
*
|
*
|
||||||
@ -5657,24 +5657,19 @@ function downloadCacheStorageSDK(archiveLocation, archivePath, options) {
|
|||||||
const fd = fs.openSync(archivePath, 'w');
|
const fd = fs.openSync(archivePath, 'w');
|
||||||
try {
|
try {
|
||||||
downloadProgress.startDisplayTimer();
|
downloadProgress.startDisplayTimer();
|
||||||
const controller = new abort_controller_1.AbortController();
|
|
||||||
const abortSignal = controller.signal;
|
|
||||||
while (!downloadProgress.isDone()) {
|
while (!downloadProgress.isDone()) {
|
||||||
const segmentStart = downloadProgress.segmentOffset + downloadProgress.segmentSize;
|
const segmentStart = downloadProgress.segmentOffset + downloadProgress.segmentSize;
|
||||||
const segmentSize = Math.min(maxSegmentSize, contentLength - segmentStart);
|
const segmentSize = Math.min(maxSegmentSize, contentLength - segmentStart);
|
||||||
downloadProgress.nextSegment(segmentSize);
|
downloadProgress.nextSegment(segmentSize);
|
||||||
const result = yield promiseWithTimeout(options.segmentTimeoutInMs || 3600000, client.downloadToBuffer(segmentStart, segmentSize, {
|
const result = yield Promise.race([client.downloadToBuffer(segmentStart, segmentSize, {
|
||||||
abortSignal,
|
concurrency: options.downloadConcurrency,
|
||||||
concurrency: options.downloadConcurrency,
|
onProgress: downloadProgress.onProgress()
|
||||||
onProgress: downloadProgress.onProgress()
|
}),
|
||||||
}));
|
timer.setTimeout(60 * 60 * 1000, 'timeout')]);
|
||||||
if (result === 'timeout') {
|
if (result === 'timeout') {
|
||||||
controller.abort();
|
throw new Error("Segment download timed out");
|
||||||
throw new Error('Aborting cache download as the download time exceeded the timeout.');
|
|
||||||
}
|
|
||||||
else if (Buffer.isBuffer(result)) {
|
|
||||||
fs.writeFileSync(fd, result);
|
|
||||||
}
|
}
|
||||||
|
fs.writeFileSync(fd, result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
@ -5685,16 +5680,6 @@ function downloadCacheStorageSDK(archiveLocation, archivePath, options) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
exports.downloadCacheStorageSDK = downloadCacheStorageSDK;
|
exports.downloadCacheStorageSDK = downloadCacheStorageSDK;
|
||||||
const promiseWithTimeout = (timeoutMs, promise) => __awaiter(void 0, void 0, void 0, function* () {
|
|
||||||
let timeoutHandle;
|
|
||||||
const timeoutPromise = new Promise(resolve => {
|
|
||||||
timeoutHandle = setTimeout(() => resolve('timeout'), timeoutMs);
|
|
||||||
});
|
|
||||||
return Promise.race([promise, timeoutPromise]).then(result => {
|
|
||||||
clearTimeout(timeoutHandle);
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
//# sourceMappingURL=downloadUtils.js.map
|
//# sourceMappingURL=downloadUtils.js.map
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
@ -40815,8 +40800,7 @@ function getDownloadOptions(copy) {
|
|||||||
const result = {
|
const result = {
|
||||||
useAzureSdk: true,
|
useAzureSdk: true,
|
||||||
downloadConcurrency: 8,
|
downloadConcurrency: 8,
|
||||||
timeoutInMs: 30000,
|
timeoutInMs: 30000
|
||||||
segmentTimeoutInMs: 3600000
|
|
||||||
};
|
};
|
||||||
if (copy) {
|
if (copy) {
|
||||||
if (typeof copy.useAzureSdk === 'boolean') {
|
if (typeof copy.useAzureSdk === 'boolean') {
|
||||||
@ -40828,14 +40812,10 @@ function getDownloadOptions(copy) {
|
|||||||
if (typeof copy.timeoutInMs === 'number') {
|
if (typeof copy.timeoutInMs === 'number') {
|
||||||
result.timeoutInMs = copy.timeoutInMs;
|
result.timeoutInMs = copy.timeoutInMs;
|
||||||
}
|
}
|
||||||
if (typeof copy.segmentTimeoutInMs === 'number') {
|
|
||||||
result.segmentTimeoutInMs = copy.segmentTimeoutInMs;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
core.debug(`Use Azure SDK: ${result.useAzureSdk}`);
|
core.debug(`Use Azure SDK: ${result.useAzureSdk}`);
|
||||||
core.debug(`Download concurrency: ${result.downloadConcurrency}`);
|
core.debug(`Download concurrency: ${result.downloadConcurrency}`);
|
||||||
core.debug(`Request timeout (ms): ${result.timeoutInMs}`);
|
core.debug(`Request timeout (ms): ${result.timeoutInMs}`);
|
||||||
core.debug(`Segment download timeout (ms): ${result.segmentTimeoutInMs}`);
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
exports.getDownloadOptions = getDownloadOptions;
|
exports.getDownloadOptions = getDownloadOptions;
|
||||||
@ -42374,7 +42354,12 @@ function clean(key)
|
|||||||
/* 578 */,
|
/* 578 */,
|
||||||
/* 579 */,
|
/* 579 */,
|
||||||
/* 580 */,
|
/* 580 */,
|
||||||
/* 581 */,
|
/* 581 */
|
||||||
|
/***/ (function(module) {
|
||||||
|
|
||||||
|
module.exports = require("timers/promises");
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
/* 582 */
|
/* 582 */
|
||||||
/***/ (function(module) {
|
/***/ (function(module) {
|
||||||
|
|
||||||
|
45
dist/save/index.js
vendored
45
dist/save/index.js
vendored
@ -5470,10 +5470,10 @@ const buffer = __importStar(__webpack_require__(293));
|
|||||||
const fs = __importStar(__webpack_require__(747));
|
const fs = __importStar(__webpack_require__(747));
|
||||||
const stream = __importStar(__webpack_require__(794));
|
const stream = __importStar(__webpack_require__(794));
|
||||||
const util = __importStar(__webpack_require__(669));
|
const util = __importStar(__webpack_require__(669));
|
||||||
|
const timer = __importStar(__webpack_require__(581));
|
||||||
const utils = __importStar(__webpack_require__(15));
|
const utils = __importStar(__webpack_require__(15));
|
||||||
const constants_1 = __webpack_require__(931);
|
const constants_1 = __webpack_require__(931);
|
||||||
const requestUtils_1 = __webpack_require__(899);
|
const requestUtils_1 = __webpack_require__(899);
|
||||||
const abort_controller_1 = __webpack_require__(106);
|
|
||||||
/**
|
/**
|
||||||
* Pipes the body of a HTTP response to a stream
|
* Pipes the body of a HTTP response to a stream
|
||||||
*
|
*
|
||||||
@ -5657,24 +5657,19 @@ function downloadCacheStorageSDK(archiveLocation, archivePath, options) {
|
|||||||
const fd = fs.openSync(archivePath, 'w');
|
const fd = fs.openSync(archivePath, 'w');
|
||||||
try {
|
try {
|
||||||
downloadProgress.startDisplayTimer();
|
downloadProgress.startDisplayTimer();
|
||||||
const controller = new abort_controller_1.AbortController();
|
|
||||||
const abortSignal = controller.signal;
|
|
||||||
while (!downloadProgress.isDone()) {
|
while (!downloadProgress.isDone()) {
|
||||||
const segmentStart = downloadProgress.segmentOffset + downloadProgress.segmentSize;
|
const segmentStart = downloadProgress.segmentOffset + downloadProgress.segmentSize;
|
||||||
const segmentSize = Math.min(maxSegmentSize, contentLength - segmentStart);
|
const segmentSize = Math.min(maxSegmentSize, contentLength - segmentStart);
|
||||||
downloadProgress.nextSegment(segmentSize);
|
downloadProgress.nextSegment(segmentSize);
|
||||||
const result = yield promiseWithTimeout(options.segmentTimeoutInMs || 3600000, client.downloadToBuffer(segmentStart, segmentSize, {
|
const result = yield Promise.race([client.downloadToBuffer(segmentStart, segmentSize, {
|
||||||
abortSignal,
|
concurrency: options.downloadConcurrency,
|
||||||
concurrency: options.downloadConcurrency,
|
onProgress: downloadProgress.onProgress()
|
||||||
onProgress: downloadProgress.onProgress()
|
}),
|
||||||
}));
|
timer.setTimeout(60 * 60 * 1000, 'timeout')]);
|
||||||
if (result === 'timeout') {
|
if (result === 'timeout') {
|
||||||
controller.abort();
|
throw new Error("Segment download timed out");
|
||||||
throw new Error('Aborting cache download as the download time exceeded the timeout.');
|
|
||||||
}
|
|
||||||
else if (Buffer.isBuffer(result)) {
|
|
||||||
fs.writeFileSync(fd, result);
|
|
||||||
}
|
}
|
||||||
|
fs.writeFileSync(fd, result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
@ -5685,16 +5680,6 @@ function downloadCacheStorageSDK(archiveLocation, archivePath, options) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
exports.downloadCacheStorageSDK = downloadCacheStorageSDK;
|
exports.downloadCacheStorageSDK = downloadCacheStorageSDK;
|
||||||
const promiseWithTimeout = (timeoutMs, promise) => __awaiter(void 0, void 0, void 0, function* () {
|
|
||||||
let timeoutHandle;
|
|
||||||
const timeoutPromise = new Promise(resolve => {
|
|
||||||
timeoutHandle = setTimeout(() => resolve('timeout'), timeoutMs);
|
|
||||||
});
|
|
||||||
return Promise.race([promise, timeoutPromise]).then(result => {
|
|
||||||
clearTimeout(timeoutHandle);
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
//# sourceMappingURL=downloadUtils.js.map
|
//# sourceMappingURL=downloadUtils.js.map
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
@ -40815,8 +40800,7 @@ function getDownloadOptions(copy) {
|
|||||||
const result = {
|
const result = {
|
||||||
useAzureSdk: true,
|
useAzureSdk: true,
|
||||||
downloadConcurrency: 8,
|
downloadConcurrency: 8,
|
||||||
timeoutInMs: 30000,
|
timeoutInMs: 30000
|
||||||
segmentTimeoutInMs: 3600000
|
|
||||||
};
|
};
|
||||||
if (copy) {
|
if (copy) {
|
||||||
if (typeof copy.useAzureSdk === 'boolean') {
|
if (typeof copy.useAzureSdk === 'boolean') {
|
||||||
@ -40828,14 +40812,10 @@ function getDownloadOptions(copy) {
|
|||||||
if (typeof copy.timeoutInMs === 'number') {
|
if (typeof copy.timeoutInMs === 'number') {
|
||||||
result.timeoutInMs = copy.timeoutInMs;
|
result.timeoutInMs = copy.timeoutInMs;
|
||||||
}
|
}
|
||||||
if (typeof copy.segmentTimeoutInMs === 'number') {
|
|
||||||
result.segmentTimeoutInMs = copy.segmentTimeoutInMs;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
core.debug(`Use Azure SDK: ${result.useAzureSdk}`);
|
core.debug(`Use Azure SDK: ${result.useAzureSdk}`);
|
||||||
core.debug(`Download concurrency: ${result.downloadConcurrency}`);
|
core.debug(`Download concurrency: ${result.downloadConcurrency}`);
|
||||||
core.debug(`Request timeout (ms): ${result.timeoutInMs}`);
|
core.debug(`Request timeout (ms): ${result.timeoutInMs}`);
|
||||||
core.debug(`Segment download timeout (ms): ${result.segmentTimeoutInMs}`);
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
exports.getDownloadOptions = getDownloadOptions;
|
exports.getDownloadOptions = getDownloadOptions;
|
||||||
@ -42374,7 +42354,12 @@ function clean(key)
|
|||||||
/* 578 */,
|
/* 578 */,
|
||||||
/* 579 */,
|
/* 579 */,
|
||||||
/* 580 */,
|
/* 580 */,
|
||||||
/* 581 */,
|
/* 581 */
|
||||||
|
/***/ (function(module) {
|
||||||
|
|
||||||
|
module.exports = require("timers/promises");
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
/* 582 */
|
/* 582 */
|
||||||
/***/ (function(module) {
|
/***/ (function(module) {
|
||||||
|
|
||||||
|
14
examples.md
14
examples.md
@ -1,7 +1,6 @@
|
|||||||
# Examples
|
# Examples
|
||||||
|
|
||||||
- [C# - NuGet](#c---nuget)
|
- [C# - NuGet](#c---nuget)
|
||||||
- [Clojure - Lein Deps](#clojure---lein-deps)
|
|
||||||
- [D - DUB](#d---dub)
|
- [D - DUB](#d---dub)
|
||||||
- [POSIX](#posix)
|
- [POSIX](#posix)
|
||||||
- [Windows](#windows)
|
- [Windows](#windows)
|
||||||
@ -81,19 +80,6 @@ steps:
|
|||||||
${{ runner.os }}-nuget-
|
${{ runner.os }}-nuget-
|
||||||
```
|
```
|
||||||
|
|
||||||
## Clojure - Lein Deps
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- name: Cache lein project dependencies
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: ~/.m2/repository
|
|
||||||
key: ${{ runner.os }}-clojure-${{ hashFiles('**/project.clj') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-clojure
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## D - DUB
|
## D - DUB
|
||||||
|
|
||||||
### POSIX
|
### POSIX
|
||||||
|
18
package-lock.json
generated
18
package-lock.json
generated
@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "cache",
|
"name": "cache",
|
||||||
"version": "3.0.7",
|
"version": "3.0.5",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "cache",
|
"name": "cache",
|
||||||
"version": "3.0.7",
|
"version": "3.0.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/cache": "^3.0.3",
|
"@actions/cache": "file:actions-cache-3.0.1.tgz",
|
||||||
"@actions/core": "^1.7.0",
|
"@actions/core": "^1.7.0",
|
||||||
"@actions/exec": "^1.1.1",
|
"@actions/exec": "^1.1.1",
|
||||||
"@actions/io": "^1.1.2"
|
"@actions/io": "^1.1.2"
|
||||||
@ -36,9 +36,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@actions/cache": {
|
"node_modules/@actions/cache": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@actions/cache/-/cache-3.0.3.tgz",
|
"resolved": "file:actions-cache-3.0.1.tgz",
|
||||||
"integrity": "sha512-kn0pZRQNFRg1IQnW/N7uTNbbLqYalvQW2bmrznn3C34LMY/rSuEmH6Uo69HDh335Q0vKs9kg/jsIarzUBKzEXg==",
|
"integrity": "sha512-ucvw0xvFpe0/vfNQ/rc11ste0nidCdBAJ5j5F01BxBqjxmGH2doVzfPlqSIGhcN7wKI074x2ATb9+7HSrTqGHg==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.2.6",
|
"@actions/core": "^1.2.6",
|
||||||
"@actions/exec": "^1.0.1",
|
"@actions/exec": "^1.0.1",
|
||||||
@ -9533,9 +9534,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/cache": {
|
"@actions/cache": {
|
||||||
"version": "3.0.3",
|
"version": "file:actions-cache-3.0.1.tgz",
|
||||||
"resolved": "https://registry.npmjs.org/@actions/cache/-/cache-3.0.3.tgz",
|
"integrity": "sha512-ucvw0xvFpe0/vfNQ/rc11ste0nidCdBAJ5j5F01BxBqjxmGH2doVzfPlqSIGhcN7wKI074x2ATb9+7HSrTqGHg==",
|
||||||
"integrity": "sha512-kn0pZRQNFRg1IQnW/N7uTNbbLqYalvQW2bmrznn3C34LMY/rSuEmH6Uo69HDh335Q0vKs9kg/jsIarzUBKzEXg==",
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"@actions/core": "^1.2.6",
|
"@actions/core": "^1.2.6",
|
||||||
"@actions/exec": "^1.0.1",
|
"@actions/exec": "^1.0.1",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cache",
|
"name": "cache",
|
||||||
"version": "3.0.7",
|
"version": "3.0.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Cache dependencies and build outputs",
|
"description": "Cache dependencies and build outputs",
|
||||||
"main": "dist/restore/index.js",
|
"main": "dist/restore/index.js",
|
||||||
@ -23,7 +23,7 @@
|
|||||||
"author": "GitHub",
|
"author": "GitHub",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/cache": "^3.0.3",
|
"@actions/cache": "file:actions-cache-3.0.1.tgz",
|
||||||
"@actions/core": "^1.7.0",
|
"@actions/core": "^1.7.0",
|
||||||
"@actions/exec": "^1.1.1",
|
"@actions/exec": "^1.1.1",
|
||||||
"@actions/io": "^1.1.2"
|
"@actions/io": "^1.1.2"
|
||||||
|
Reference in New Issue
Block a user