feat: measure and display user quota (#72)

This commit is contained in:
Thomas Durieux
2021-09-06 14:53:42 +02:00
committed by GitHub
parent 91014fbae2
commit 20e8d533f4
9 changed files with 199 additions and 33 deletions
+8 -5
View File
@@ -164,18 +164,21 @@ async function connect(db) {
console.error(`Repository ${r.fullName} is not found (renamed)`); console.error(`Repository ${r.fullName} is not found (renamed)`);
} }
} }
let size = 0; let size = { storage: 0, file: 0 };
function recursiveCount(files) { function recursiveCount(files) {
let total = 0; const out = { storage: 0, file: 0 };
for (const name in files) { for (const name in files) {
const file = files[name]; const file = files[name];
if (file.size && file.sha && parseInt(file.size) == file.size) { if (file.size && file.sha && parseInt(file.size) == file.size) {
total += file.size as number; out.storage += file.size as number;
out.file++;
} else if (typeof file == "object") { } else if (typeof file == "object") {
total += recursiveCount(file); const r = recursiveCount(file);
out.storage += r.storage;
out.file += r.file;
} }
} }
return total; return out;
} }
if (r.originalFiles) { if (r.originalFiles) {
+1
View File
@@ -9,6 +9,7 @@
"repoId_already_used": "The repository id is already used.", "repoId_already_used": "The repository id is already used.",
"invalid_repoId": "The format of the repository id is invalid.", "invalid_repoId": "The format of the repository id is invalid.",
"branch_not_specified": "The branch is not specified.", "branch_not_specified": "The branch is not specified.",
"branch_not_found": "The branch of the repository cannot be found.",
"options_not_provided": "Anonymization options are mandatory.", "options_not_provided": "Anonymization options are mandatory.",
"invalid_terms_format": "Terms are in an invalid format.", "invalid_terms_format": "Terms are in an invalid format.",
"unable_to_anonymize": "An error happened during the anonymization process. Please try later or report the issue.", "unable_to_anonymize": "An error happened during the anonymization process. Please try later or report the issue.",
+62 -1
View File
@@ -152,6 +152,67 @@
</div> </div>
</div> </div>
</div> </div>
<div class="mt-1 mr-1" style="margin-top: -0.25rem !important">
<strong>Repository</strong>
<div class="progress" style="min-width: 150px">
<div
class="progress-bar"
ng-class="{'progress-bar-striped progress-bar-animated w-100 bg-dark': !quota, 'bg-success': quota.repository.percent < 25 || quota.repository.total == 0, 'bg-danger': quota.repository.percent > 95 && quota.repository.total > 0, 'bg-warning': quota.repository.percent > 75 && quota.repository.total > 0 }"
role="progressbar"
style="width: {{quota.repository.percent}}%;"
aria-valuenow="{{quota.repository.used}}"
aria-valuemin="0"
aria-valuemax="{{quota.repository.total}}"
>
<span ng-show="quota"
>{{quota.repository.used |
number}}/{{quota.repository.total}}</span
>
</div>
</div>
</div>
<div class="mt-1 mr-1" style="margin-top: -0.25rem !important">
<strong>Storage</strong>
<div class="progress" style="min-width: 150px">
<div
class="progress-bar"
ng-class="{'progress-bar-striped progress-bar-animated w-100 bg-dark': !quota, 'bg-success': quota.storage.percent < 25 || quota.storage.total == 0, 'bg-danger': quota.storage.percent > 95 && quota.storage.total > 0, 'bg-warning': quota.storage.percent > 75 && quota.storage.total > 0 }"
role="progressbar"
style="width: {{quota.storage.percent}}%;"
aria-valuenow="{{quota.storage.used}}"
aria-valuemin="0"
aria-valuemax="{{quota.storage.total}}"
>
<span ng-show="quota"
>{{quota.storage.used |
humanFileSize}}/{{quota.storage.total|
humanFileSize}}</span
>
</div>
</div>
</div>
<div class="mt-1 mr-1" style="margin-top: -0.25rem !important">
<strong>File</strong>
<div class="progress" style="min-width: 150px">
<div
class="progress-bar"
ng-class="{'progress-bar-striped progress-bar-animated w-100 bg-dark': !quota, 'bg-success': quota.file.percent < 25 || quota.file.total == 0, 'bg-danger': quota.file.percent > 95 && quota.file.total > 0, 'bg-warning': quota.file.percent > 75 && quota.file.total > 0 }"
role="progressbar"
style="width: {{quota.file.percent}}%;"
aria-valuenow="{{quota.file.used}}"
aria-valuemin="0"
aria-valuemax="{{quota.file.total}}"
>
<span ng-show="quota"
>{{quota.file.used | number}}/{{quota.file.total ||
"∞"}}</span
>
</div>
</div>
</div>
</div> </div>
</div> </div>
</form> </form>
@@ -235,7 +296,7 @@
data-toggle="tooltip" data-toggle="tooltip"
data-placement="bottom" data-placement="bottom"
> >
<i class="fas fa-database"></i> {{::repo.size | <i class="fas fa-database"></i> {{::repo.size.storage |
humanFileSize}}</span humanFileSize}}</span
> >
<span <span
+52
View File
@@ -1,4 +1,56 @@
<div class="container py-4"> <div class="container py-4">
<h2>Quota</h2>
<h3>Quota</h3>
<h5>Repository</h5>
<div class="progress">
<div
class="progress-bar"
ng-class="{'progress-bar-striped progress-bar-animated w-100 bg-dark': !quota, 'bg-success': quota.repository.percent < 25 || quota.repository.total == 0, 'bg-danger': quota.repository.percent > 95 && quota.repository.total > 0, 'bg-warning': quota.repository.percent > 75 && quota.repository.total > 0 }"
role="progressbar"
style="width: {{quota.repository.percent}}%;"
aria-valuenow="{{quota.repository.used}}"
aria-valuemin="0"
aria-valuemax="{{quota.repository.total}}"
>
<span ng-show="quota"
>{{quota.repository.used | number}}/{{quota.repository.total}}</span
>
</div>
</div>
<h5>Storage</h5>
<div class="progress">
<div
class="progress-bar"
ng-class="{'progress-bar-striped progress-bar-animated w-100 bg-dark': !quota, 'bg-success': quota.storage.percent < 25 || quota.storage.total == 0, 'bg-danger': quota.storage.percent > 95 && quota.storage.total > 0, 'bg-warning': quota.storage.percent > 75 && quota.storage.total > 0 }"
role="progressbar"
style="width: {{quota.storage.percent}}%;"
aria-valuenow="{{quota.storage.used}}"
aria-valuemin="0"
aria-valuemax="{{quota.storage.total}}"
>
<span ng-show="quota"
>{{quota.storage.used | humanFileSize}}/{{quota.storage.total|
humanFileSize}}</span
>
</div>
</div>
<h5>File</h5>
<div class="progress">
<div
class="progress-bar"
ng-class="{'progress-bar-striped progress-bar-animated w-100 bg-dark': !quota, 'bg-success': quota.file.percent < 25 || quota.file.total == 0, 'bg-danger': quota.file.percent > 95 && quota.file.total > 0, 'bg-warning': quota.file.percent > 75 && quota.file.total > 0 }"
role="progressbar"
style="width: {{quota.file.percent}}%;"
aria-valuenow="{{quota.file.used}}"
aria-valuemin="0"
aria-valuemax="{{quota.file.total}}"
>
<span ng-show="quota"
>{{quota.file.used | number}}/{{quota.file.total || "∞"}}</span
>
</div>
</div>
<h2>Default anonymization options</h2> <h2>Default anonymization options</h2>
<form class="form needs-validation" name="default" novalidate> <form class="form needs-validation" name="default" novalidate>
<!-- Terms --> <!-- Terms -->
+17 -7
View File
@@ -406,6 +406,7 @@ angular
$http.get("/api/user").then( $http.get("/api/user").then(
(res) => { (res) => {
if (res) $scope.user = res.data; if (res) $scope.user = res.data;
getQuota();
}, },
() => { () => {
$scope.user = null; $scope.user = null;
@@ -425,6 +426,22 @@ angular
); );
} }
getOptions(); getOptions();
function getQuota() {
$http.get("/api/user/quota").then((res) => {
$scope.quota = res.data;
$scope.quota.storage.percent = $scope.quota.storage.total
? ($scope.quota.storage.used * 100) / $scope.quota.storage.total
: 100;
$scope.quota.file.percent = $scope.quota.file.total
? ($scope.quota.file.used * 100) / $scope.quota.file.total
: 100;
$scope.quota.repository.percent = $scope.quota.repository.total
? ($scope.quota.repository.used * 100) /
$scope.quota.repository.total
: 100;
}, console.error);
}
getQuota();
function getMessage() { function getMessage() {
$http.get("/api/message").then( $http.get("/api/message").then(
@@ -605,13 +622,6 @@ angular
} }
getRepositories(); getRepositories();
function getQuota() {
$http.get("/api/user/quota").then((res) => {
$scope.quota = res.data;
}, console.error);
}
// getQuota();
$scope.removeRepository = (repo) => { $scope.removeRepository = (repo) => {
if ( if (
confirm( confirm(
+33 -14
View File
@@ -82,7 +82,7 @@ export default class Repository {
} }
const files = await this.source.getFiles(); const files = await this.source.getFiles();
this._model.originalFiles = files; this._model.originalFiles = files;
this._model.size = 0; this._model.size = { storage: 0, file: 0 };
await this.computeSize(); await this.computeSize();
await this._model.save(); await this._model.save();
@@ -149,14 +149,21 @@ export default class Repository {
const branch = this.source.branch; const branch = this.source.branch;
if ( if (
branch.commit == branch.commit ==
branches.filter((f) => f.name == branch.name)[0].commit branches.filter((f) => f.name == branch.name)[0]?.commit
) { ) {
console.log(`${this._model.repoId} is up to date`); console.log(`${this._model.repoId} is up to date`);
return; return;
} }
this._model.source.commit = branches.filter( this._model.source.commit = branches.filter(
(f) => f.name == branch.name (f) => f.name == branch.name
)[0].commit; )[0]?.commit;
if (!this._model.source.commit) {
console.error(
`${branch.name} for ${this.source.githubRepository.fullName} is not found`
);
throw new Error("branch_not_found");
}
this._model.anonymizeDate = new Date(); this._model.anonymizeDate = new Date();
console.log( console.log(
`${this._model.repoId} will be updated to ${this._model.source.commit}` `${this._model.repoId} will be updated to ${this._model.source.commit}`
@@ -218,7 +225,7 @@ export default class Repository {
*/ */
private async resetSate(status?: RepositoryStatus) { private async resetSate(status?: RepositoryStatus) {
if (status) this._model.status = status; if (status) this._model.status = status;
this._model.size = 0; this._model.size = { storage: 0, file: 0 };
this._model.originalFiles = null; this._model.originalFiles = null;
return Promise.all([ return Promise.all([
this._model.save(), this._model.save(),
@@ -227,27 +234,39 @@ export default class Repository {
} }
/** /**
* Compute the size of the repository in bite. * Compute the size of the repository in term of storage and number of files.
* *
* @returns The size of the repository in bite * @returns The size of the repository in bite
*/ */
async computeSize(): Promise<number> { async computeSize(): Promise<{
if (this._model.status != "ready") return 0; /**
if (this._model.size) return this._model.size; * Size of the repository in bit
*/
storage: number;
/**
* The number of files
*/
file: number;
}> {
if (this._model.status != "ready") return { storage: 0, file: 0 };
if (this._model.size.file) return this._model.size;
function recursiveCount(files) { function recursiveCount(files) {
let total = 0; const out = { storage: 0, file: 0 };
for (const name in files) { for (const name in files) {
const file = files[name]; const file = files[name];
if (file.size) { if (file.size) {
total += file.size as number; out.storage += file.size as number;
out.file++;
} else if (typeof file == "object") { } else if (typeof file == "object") {
total += recursiveCount(file); const r = recursiveCount(file);
out.storage += r.storage;
out.file += r.file;
} }
} }
return total; return out;
} }
const files = await this.files({ force: false }); const files = await this.files();
this._model.size = recursiveCount(files); this._model.size = recursiveCount(files);
await this._model.save(); await this._model.save();
return this._model.size; return this._model.size;
@@ -291,7 +310,7 @@ export default class Repository {
} }
get size() { get size() {
if (this._model.status != "ready") return 0; if (this._model.status != "ready") return { storage: 0, file: 0 };
return this._model.size; return this._model.size;
} }
@@ -46,8 +46,14 @@ const AnonymizedRepositorySchema = new Schema({
default: new Date(), default: new Date(),
}, },
size: { size: {
type: Number, storage: {
default: 0, type: Number,
default: 0,
},
file: {
type: Number,
default: 0,
},
}, },
}); });
@@ -34,7 +34,10 @@ export interface IAnonymizedRepository {
}; };
pageView: number; pageView: number;
lastView: Date; lastView: Date;
size: number; size: {
storage: number;
file: number;
};
} }
export interface IAnonymizedRepositoryDocument export interface IAnonymizedRepositoryDocument
+14 -3
View File
@@ -29,14 +29,25 @@ router.get("/", async (req: express.Request, res: express.Response) => {
router.get("/quota", async (req: express.Request, res: express.Response) => { router.get("/quota", async (req: express.Request, res: express.Response) => {
try { try {
const user = await getUser(req); const user = await getUser(req);
const repositories = await user.getRepositories();
const sizes = await Promise.all( const sizes = await Promise.all(
(await user.getRepositories()) repositories
.filter((r) => r.status == "ready") .filter((r) => r.status == "ready")
.map((r) => r.computeSize()) .map((r) => r.computeSize())
); );
res.json({ res.json({
used: sizes.reduce((sum, i) => sum + i, 0), storage: {
total: config.DEFAULT_QUOTA, used: sizes.reduce((sum, i) => sum + i.storage, 0),
total: config.DEFAULT_QUOTA,
},
file: {
used: sizes.reduce((sum, i) => sum + i.file, 0),
total: 0,
},
repository: {
used: repositories.length,
total: 20,
},
}); });
} catch (error) { } catch (error) {
handleError(error, res); handleError(error, res);