aiiinotate 0.6.2 → 0.8.0

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 CHANGED
@@ -6,7 +6,7 @@ aiiinotate is a fast and lightweight annotation server for IIIF. It relies on `n
6
6
 
7
7
  ## PROD USAGE
8
8
 
9
- ### Install
9
+ ### Install
10
10
 
11
11
  1. **Install mongodb**.
12
12
  - see [dev installation script for help](./scripts/setup_mongodb.sh)
@@ -17,9 +17,37 @@ aiiinotate is a fast and lightweight annotation server for IIIF. It relies on `n
17
17
  npm install aiiinotate
18
18
  ```
19
19
 
20
- ### Setup the app
20
+ ### Env definition
21
21
 
22
- 0. **Setup your `.env`** file after [.env.template](./config/.env.template).
22
+ #### Basic definition
23
+
24
+ Copy [`config/.env.template`](./config/.env.template) to `.env` and edit it.
25
+
26
+ #### Runtime env sourcing
27
+
28
+ Once the package is installed, it must access variables from the .env file. However, running `aiiinotate <commands>` creates a subscript which means **you can't `source` an `.env` file**.
29
+
30
+ ```bash
31
+ # THIS WILL FAIL: `aiiinotate` is executed in a subscript that doesn't inherit from the variables fetched in `source`.
32
+ source /path/to/.env && aiiinotate <command>
33
+ ```
34
+
35
+ #### The solutions: do either:
36
+ 1. use `dotenvx` to inject variables:
37
+ ```bash
38
+ npx dotenvx run -f /path/to/.env -- aiiinotate <command>
39
+ ```
40
+ 2. manually export variables:
41
+ ```bash
42
+ set -a
43
+ source /path/to/.env
44
+ set +a
45
+ aiiinotate <command>
46
+ ```
47
+
48
+ For clarity, we omit env sourcing from the below commands.
49
+
50
+ ### Setup the app
23
51
 
24
52
  1. **Start `mongod`**
25
53
 
@@ -30,38 +58,41 @@ sudo systemctl start mongod
30
58
  2. **Create and configure the database**
31
59
 
32
60
  ```bash
33
- aiiinotate --env <path-to-your-env-file> -- migrate apply
61
+ aiiinotate migrate apply
34
62
  ```
35
63
 
36
64
  ### Usage
37
65
 
38
66
  All commands are accessible through a CLI (`./src/cli`).
39
67
 
40
- #### Run the app
68
+ #### Run the app
41
69
 
42
70
  ```bash
43
- aiiinotate --env <path-to-your-env-file> -- serve prod
44
- # or
45
- aiiinotate --env <path-to-your-env-file> -- serve dev
71
+ aiiinotate serve prod
46
72
  ```
47
73
 
48
- #### Run administration commands
74
+ #### Run the CLI
49
75
 
50
76
  The base command is:
51
77
 
52
78
  ```bash
53
- aiiinotate --env <path-to-your-env-file> -- <command>
79
+ aiiinotate -- <command>
54
80
  ```
55
81
 
56
82
  It will give full access to the CLI interface of Aiiinotate. Run `aiiinotate --help` for more info.
57
83
 
58
- 1. Import data - TODO
84
+ For more information, see [the CLI docs](https://github.com/Aikon-platform/aiiinotate/blob/main/docs/cli.md).
85
+
86
+ #### Import data
87
+
88
+ See [the CLI docs](https://github.com/Aikon-platform/aiiinotate/blob/main/docs/cli.md).
89
+
59
90
 
60
91
  ---
61
92
 
62
93
  ## DEV USAGE
63
94
 
64
- ### Install
95
+ ### Install
65
96
 
66
97
  ```bash
67
98
  # clone the repo
@@ -84,15 +115,15 @@ npm i
84
115
 
85
116
  After installing, some setup must be done
86
117
 
87
- 0. **Setup your `.env`** file after [.env.template](./config/.env.template) and place it at `./config/.env`.
118
+ 1. **Setup your `.env`** file after [`config/.env.template`](./config/.env.template) and place it at `./config/.env`.
88
119
 
89
- 1. **Start `mongod`**
120
+ 2. **Start `mongod`**
90
121
 
91
122
  ```bash
92
123
  sudo systemctl start mongod
93
124
  ```
94
125
 
95
- 2. **Configure the database**
126
+ 3. **Configure the database**
96
127
 
97
128
  ```bash
98
129
  npm run migrate apply
@@ -112,32 +143,32 @@ npm run dev
112
143
  npm run prod
113
144
  ```
114
145
 
115
- - **Test the app**
146
+ - **Test the app**. NOTE: the tests will probably fail if you set the env variable `AIIINOTATE_STRICT_MODE` to `true`.
116
147
 
117
148
  ```bash
118
149
  npm run test
119
150
  ```
120
151
 
121
- - **Run the CLI**
152
+ - **Run the CLI**. (see [the CLI docs](https://github.com/Aikon-platform/aiiinotate/blob/dev/docs/cli.md) for more info)
153
+
122
154
 
123
155
  ```bash
124
156
  npm cli
125
157
  ```
126
158
 
127
- - **Process migrations**
159
+ - **Process migrations**. (see [the CLI docs](https://github.com/Aikon-platform/aiiinotate/blob/dev/docs/cli.md) for more info)
128
160
 
129
161
  ```bash
130
- # create a new migration. NOTE: the `--` is necessary !
131
- npm run migrate make -- --migration-name <your migration name>
162
+ # NOTE: the `--` is necessary !
163
+ npm run migrate -- <command> <arguments?>
164
+ ```
132
165
 
133
- # apply all pending migrations
134
- npm run migrate apply
135
166
 
136
- # revert the last migration
137
- npm run migrate revert
167
+ - **Import data**. (see [the CLI docs](https://github.com/Aikon-platform/aiiinotate/blob/main/docs/cli.md) for more info)
138
168
 
139
- # revert all migrations
140
- npm run migrate revert-all
169
+ ```bash
170
+ # NOTE: the `--` is necessary !
171
+ npm run cli -- import <arguments>
141
172
  ```
142
173
 
143
174
  ---
package/cli/import.js CHANGED
@@ -1,40 +1,17 @@
1
1
  import { Command, Option, Argument } from "commander";
2
2
 
3
- import Annotations2 from "#annotations/annotations2.js";
4
- import Annotations3 from "#annotations/annotations3.js";
5
- import { getFilesToProcess, fileRead } from "#cli/utils/io.js";
6
- import loadMongoClient from "#cli/utils/mongoClient.js";
3
+ import { fileRead, parseImportInputFile } from "#cli/utils/io.js";
4
+ import FastifyClient from "#cli/utils/fastifyClient.js";
5
+ import ProgressBar from "#cli/utils/progressbar.js";
6
+ import logger from "#utils/logger.js";
7
+ import { inspectObj } from "#utils/utils.js";
7
8
 
8
- ////////////////////////////////////////
9
-
10
- // allowed imports
11
- const importTypes = [
12
- "annotation", // import a single annotation
13
- "annotation-list", // import a IIIF 2.x annotationList
14
- "annotation-page", // import a IIIF 3.x annotationPage
15
- "manifest", // import a single manifest
16
- // "annotation-array", // import a JSON array of IIIF annotations
17
- // "manifest-array" // import a json array of manifests
18
- ]
19
-
20
- // allowed import types per IIIF version
21
- const allowedImportTypes = {
22
- 2: ["annotation", "annotation-list", "manifest"],
23
- 3: ["annotation", "annotation-page", "manifest"]
24
- }
25
-
26
- const checkAllowedImportType = (iiifVersion, dataType) => {
27
- if (
28
- ! allowedImportTypes[iiifVersion].includes(dataType)
29
- ) {
30
- console.error(`${checkAllowedImportType.name}: forbidden import type '${dataType}' for IIIF version '${iiifVersion}'. allowed import types are: ${allowedImportTypes[iiifVersion]}`);
31
- process.exit(1);
32
- };
9
+ /** @typedef {import("#types").FastifyInstanceType} FastifyInstanceType */
33
10
 
34
- }
11
+ ////////////////////////////////////////
35
12
 
36
13
  const notImplementedExit = (method) => {
37
- console.log(`\n\nERROR: import is not implemented '${method}'`);
14
+ logger.error(`\n\nERROR: import is not implemented '${method}'`);
38
15
  process.exit(1);
39
16
  }
40
17
 
@@ -42,28 +19,42 @@ const parseNumber = (x) => Number(x);
42
19
 
43
20
  ////////////////////////////////////////
44
21
 
45
- async function importAnnotationPage(annotations3, fileArr, iiifVersion) {
22
+ /**
23
+ * @param {FastifyInstanceType} fastifyClient
24
+ * @param {string[]} fileArr - array of full paths to annotationLists to insert.
25
+ */
26
+ async function importAnnotationPage(fastifyClient, fileArr) {
46
27
  notImplementedExit(`${importAnnotationPage.name} is not implemented`)
47
28
  }
48
29
 
49
30
  /**
50
- *
51
- * @param {Annotations2} annotations2
52
- * @param {string[]} fileArr
53
- * @param {2|3} iiifVersion
31
+ * @param {FastifyClient} fastifyClient
32
+ * @param {string[]} fileArr - array of full paths to annotationLists to insert.
54
33
  */
55
- async function importAnnotationList(annotations2, fileArr, iiifVersion) {
56
- // RUN THE SCRIPT:
57
- // > npm run migrate-revert && npm run migrate-apply && npm run cli import -- annotation-list -i 2 -f ./data/aikon_wit9_man11_anno165_annotation_list.jsonld
34
+ async function importAnnotationList(fastifyClient, fileArr) {
35
+ // fileArr = fileArr.slice(0,10)
36
+
37
+ const pb = new ProgressBar({ desc: "importing annotations", total: fileArr.length});
38
+ const importErrors = [];
58
39
  let totalImports = 0
59
40
 
60
- for (const file of fileArr) {
61
- const annotationList = JSON.parse(await fileRead(file));
62
- const result = await annotations2.insertAnnotationList(annotationList);
63
- totalImports += Object.keys(result).length;
41
+ for ( const [i, file] of fileArr.entries() ) {
42
+ const
43
+ annotationList = JSON.parse(fileRead(file)),
44
+ [statusCode, resultPromise] = await fastifyClient.importAnnotationList(annotationList),
45
+ result = await resultPromise;
46
+ if ( statusCode <= 299 ) {
47
+ totalImports += result.insertedCount;
48
+ } else {
49
+ importErrors.push(file);
50
+ }
51
+ pb.update(i)
64
52
  }
65
53
 
66
- console.log(`\n\nDONE: imported ${totalImports} annotations into Aiiinotate !`);
54
+ if ( importErrors.length ) {
55
+ logger.info(`There were problems importing annotations from the following ${importErrors.length} annotation lists: ${inspectObj(importErrors, -1)}`)
56
+ }
57
+ logger.info(`Imported ${totalImports} annotations into Aiiinotate !`);
67
58
  return
68
59
  }
69
60
 
@@ -72,51 +63,33 @@ async function importAnnotationList(annotations2, fileArr, iiifVersion) {
72
63
  /**
73
64
  * run the cli
74
65
  * @param {import('commander').Command} command
75
- * @param {string} dataType: one of importTypes
76
66
  * @param {object} options
77
67
  */
78
- async function action(command, dataType, options) {
79
- const mongoClient = loadMongoClient();
68
+ async function action(command, options) {
80
69
 
81
70
  /** @type {2 | 3} */
82
71
  const iiifVersion = options.iiifVersion;
83
72
  /** @type {string[]} */
84
- const files = options.files;
85
- /** @type {boolean} */
86
- const listFiles = options.listFiles;
87
-
88
- checkAllowedImportType(iiifVersion, dataType);
89
-
90
- const filesToProcess = getFilesToProcess(files, listFiles);
73
+ const inputFile = options.file;
91
74
 
92
- const annotations2 = new Annotations2(
93
- mongoClient,
94
- mongoClient.db()
95
- );
75
+ const fastifyClient = new FastifyClient();
76
+ await fastifyClient.build();
96
77
 
97
78
  // run
98
- switch (dataType) {
99
- case ("annotation-list"):
100
- await importAnnotationList(annotations2, filesToProcess, iiifVersion);
101
- break;
102
- default:
103
- notImplementedExit(dataType);
79
+ const filesToProcess = await parseImportInputFile(inputFile);
80
+ if ( iiifVersion===2 ) {
81
+ await importAnnotationList(fastifyClient, filesToProcess);
82
+ } else {
83
+ await importAnnotationPage(fastifyClient, filesToProcess);
104
84
  }
105
- mongoClient.close();
106
-
85
+ // exit
86
+ await fastifyClient.stop();
107
87
  }
108
88
 
109
89
  /////////////////////////////////////////
110
90
 
111
91
  /** define the cli */
112
- function makeImportCommand(mongoClient) {
113
-
114
- // argument and option name syntax:
115
- // --opt-name <requiredVal> => you mst provide a value after --opt-name
116
- // --opt-name [optionalVal] => if `optionalVal` is not provided, --opt-name will be treated as boolean
117
- const dataTypeArg =
118
- new Argument("<data-type>", "type of data to import")
119
- .choices(importTypes);
92
+ function makeImportCommand() {
120
93
 
121
94
  const versionOpt =
122
95
  new Option("-i, --iiif-version <version>", "IIIF version")
@@ -125,19 +98,14 @@ function makeImportCommand(mongoClient) {
125
98
  .makeOptionMandatory();
126
99
 
127
100
  const filesOpt =
128
- new Option("-f, --files <files...>", "files to process, either as: space-separated filepath(s) to the JSON(s) OR path to a file containing a list of paths to JSON files to process (1 line per path)")
101
+ new Option("-f, --file <file>", "file containing paths to AnnotationLists or AnnotationPages to process (1 line per path)")
129
102
  .makeOptionMandatory();
130
103
 
131
- const listFilesOpt =
132
- new Option("-l, --list-files", "flag indicating that --files points to a file containing a list of JSON files to process (1 line per path)")
133
-
134
104
  return new Command("import")
135
105
  .description("import data into aiiinotate")
136
- .addArgument(dataTypeArg)
137
106
  .addOption(filesOpt)
138
107
  .addOption(versionOpt)
139
- .addOption(listFilesOpt)
140
- .action((dataType, options, command) => action(mongoClient, command, dataType, options))
108
+ .action((options, command) => action(command, options))
141
109
  }
142
110
 
143
111
  export default makeImportCommand;
package/cli/index.js CHANGED
@@ -13,7 +13,6 @@ import { Command, Option } from "commander";
13
13
 
14
14
  // import dotenvx from "dotenvx";
15
15
 
16
- import makeMongoClient from "#cli/utils/mongoClient.js";
17
16
  import makeImportCommand from "#cli/import.js";
18
17
  import makeMigrateCommand from "#cli/migrate.js";
19
18
  import makeServeCommand from "#cli/serve.js";
@@ -29,25 +28,11 @@ function makeCli() {
29
28
  Run individual commands to see command-specific help.
30
29
  `.replace(/^\s+/gm, "");
31
30
 
32
- const envFileOpt =
33
- new Option("--env <env-file>", "path to .env file").makeOptionMandatory();
34
-
35
- // NOTE: how do we load the env variables ? it's a bit unorthodox:
36
- // - the CLI requires to use a global `--env` option with a path to the .env file.
37
- // - we use the hook `preAction` that is called before any (sub-)command's `action` function is called.
38
- // - in the `preAction` hook, we call `loadEnv` that will load all env files defined in the `.env` file.
39
- // - this way, all env variables will be defined in the subcommand's `action` methods (and children).
40
- // this is based on https://github.com/tj/commander.js/issues/563#issuecomment-520112985 but replaxes `.on` with `.action`, which works better.
41
- // WARNING: this means that the env variables can't be used in (sub-)commands BEFORE `action()` has been called.
31
+ // NOTE: before running, it is necessary to load env variables.
42
32
  const cli = new Command();
43
33
  cli
44
34
  .name("aiiinotate")
45
35
  .description(desc)
46
- .usage("--env <path-to-your-env-file> -- <command> [options]")
47
- .addOption(envFileOpt)
48
- .hook("preAction", (thisCommand, actionCommand) => {
49
- loadEnv(thisCommand.opts().env);
50
- })
51
36
  .addCommand(makeServeCommand())
52
37
  .addCommand(makeImportCommand())
53
38
  .addCommand(makeMigrateCommand());
@@ -56,4 +41,4 @@ function makeCli() {
56
41
  return cli;
57
42
  }
58
43
 
59
- await makeCli();
44
+ makeCli();
package/cli/migrate.js CHANGED
@@ -112,7 +112,8 @@ function makeMigrateCommand() {
112
112
  new Argument("<migration-op>", "name of migration operation").choices(allowedMigrateOp);
113
113
 
114
114
  const migrationNameOpt =
115
- new Option("-n, --migration-name <name>", "name of migration (for 'make' argument)");
115
+ new Option("-n, --migration-name <name>", "name of migration (for 'make' argument)")
116
+ .makeOptionMandatory();
116
117
 
117
118
  return new Command("migrate")
118
119
  .description("run database migrations")
package/cli/serve.js CHANGED
@@ -2,13 +2,13 @@ import serve from "#src/server.js";
2
2
 
3
3
  import { Command, Option, Argument } from "commander";
4
4
 
5
- /** @typedef {import("#types").RerveModeType} RerveModeType */
5
+ /** @typedef {import("#types").ServeModeType} ServeModeType */
6
6
 
7
- const serveModeValues = ["test", "dev", "prod"];
7
+ const serveModeValues = ["dev", "prod"];
8
8
 
9
9
  /**
10
10
  * @param {import('commander').Command} command
11
- * @param {RerveModeType} serveMode
11
+ * @param {ServeModeType} serveMode
12
12
  */
13
13
  async function action(command, serveMode) {
14
14
  await serve(serveMode);
@@ -0,0 +1,74 @@
1
+ import build from "#src/app.js";
2
+
3
+ /** @typedef {import("#types").FastifyInstanceType} FastifyInstanceType */
4
+ /** @typedef {import("#types").FastifyReplyType} FastifyReplyType */
5
+ /** @typedef {import("#types").InsertResponseType} InsertResponseType */
6
+
7
+ /**
8
+ * a client to interact with the fastify app.
9
+ *
10
+ * @example
11
+ * import FastifyClient from "#cli/utils/fastifyClient.js";
12
+ * const fastify = new FastifyClient();
13
+ * // load the fastify instance asynchronously
14
+ * await fastify.build();
15
+ * // run operations
16
+ * await fastify.importAnnotationList(...);
17
+ */
18
+ class FastifyClient {
19
+ constructor () { }
20
+
21
+ // NOTE: instanciating this.fastify is asynchronous and a constructor must be synchronous,
22
+ // so we use a builder to set this.fastify, following this pattern: https://stackoverflow.com/a/43433773
23
+ async build() {
24
+ /** @type {FastifyInstanceType} */
25
+ this.fastify = await build("default");
26
+ // TODO find way to actually completely disable logging because it makes the CLI UI uglyyy !
27
+ // log only fastify errors
28
+ this.fastify.log.level = "error";
29
+ }
30
+
31
+ async stop() {
32
+ await this.fastify.close();
33
+ this.fastify.log.info("fastify instance successfully closed.");
34
+ }
35
+
36
+ /**
37
+ * @param {string} route
38
+ * @returns {Promise<FastifyReplyType>}
39
+ */
40
+ injectGet(route) {
41
+ return this.fastify.inject({
42
+ method: "GET",
43
+ url: route,
44
+ })
45
+ }
46
+
47
+ /**
48
+ * @param {string} route
49
+ * @param {object} payload
50
+ * @returns {Promise<FastifyReplyType>}
51
+ */
52
+ injectPost(route, payload) {
53
+ return this.fastify.inject({
54
+ method: "POST",
55
+ url: route,
56
+ payload: payload
57
+ })
58
+ }
59
+
60
+ async importAnnotationPage(annotationPage) {
61
+ // TODO
62
+ }
63
+
64
+ /**
65
+ * @param {object} annotationList - IIIF 2 annotationList
66
+ * @returns {[number, Promise<InsertResponseType>]} - [ statusCode, response ]
67
+ */
68
+ async importAnnotationList(annotationList) {
69
+ const r = await this.injectPost("/annotations/2/createMany", annotationList);
70
+ return [ r.statusCode, r.json() ];
71
+ }
72
+ }
73
+
74
+ export default FastifyClient;
package/cli/utils/io.js CHANGED
@@ -54,53 +54,22 @@ function fileArrayValidate (fileArr) {
54
54
 
55
55
  /**
56
56
  * `file` is a path to a file containing paths to other files (1 file per line).
57
- * validate all paths and return them as absolute paths
57
+ * parse the file and return an array of absolute paths to files.
58
58
  * @param {str} file
59
- * @returns {string[]}
59
+ * @returns {Promise<string[]>}
60
60
  */
61
- async function getFilesInListFile(file) {
61
+ async function parseImportInputFile(file) {
62
62
  // read `file` split it by lines, remove empty lines
63
63
  const fileArr =
64
64
  fileRead(file)
65
65
  .split("\n")
66
66
  .filter(l => !l.match(/^\s*$/g));
67
- return fileArrayValidate(fileArr);
68
- }
69
-
70
- /**
71
- * get the files to import and return them as absolute paths
72
- *
73
- * `fileArr` is a list of paths to either:
74
- * - (fileArr=false) JSON files to import
75
- * - (fileArr=true) text files containing paths to the JSONS to import
76
- * => take `fileArr`, validate that all files exist, extract all filepaths
77
- * from `fileArr`, and return the array of actual JSON paths to process.
78
- *
79
- * @param {string[]} fileArr
80
- * @param {boolean} listFiles
81
- * @returns {string[]} the list of existing files to process.
82
- */
83
- function getFilesToProcess(fileArr, listFiles=false) {
84
- let filesToProcess = fileArrayValidate(fileArr);
85
-
86
- // if `listFile`, open the files containing paths of files to proces, and redo the same validation process for each file in a list file.
87
- if ( listFiles ) {
88
- let filesInListFiles = []
89
-
90
- filesToProcess.map((theListFile) => {
91
- const files = getFilesInListFile(theListFile);
92
- filesInListFiles = filesInListFiles.concat(files);
93
- })
94
-
95
- filesToProcess = [...new Set(filesInListFiles)]; // deduplicate
96
- }
97
-
98
- return filesToProcess
67
+ return [...new Set(fileArrayValidate(fileArr))];
99
68
  }
100
69
 
101
70
  export {
102
71
  fileRead,
103
72
  fileOk,
104
73
  fileArrayValidate,
105
- getFilesToProcess
74
+ parseImportInputFile
106
75
  }
@@ -1,13 +1,15 @@
1
1
  import { MongoClient } from "mongodb";
2
2
 
3
+ import { MONGODB_CONNSTRING, MONGODB_DB } from "#constants";
4
+
3
5
  /**
4
6
  * load a mongo client and connect it to the database. exists if there's an error
5
7
  * @returns {import("mongodb").MongoClient}
6
8
  */
7
9
  function loadMongoClient() {
8
10
  try {
9
- const client = new MongoClient(process.env.MONGODB_CONNSTRING);
10
- client.db(process.env.MONGODB_DB);
11
+ const client = new MongoClient(MONGODB_CONNSTRING);
12
+ client.db(MONGODB_DB);
11
13
  return client;
12
14
  } catch (err) {
13
15
  console.error(`mongoClient: could not connect to DB because of error ${err.message}`);
@@ -0,0 +1,86 @@
1
+ /**
2
+ * a progress bar
3
+ * @example
4
+ * const total = 100;
5
+ * const ms = 10;
6
+ * const pb = new ProgressBar({ desc: "Progressbar example", total: total });
7
+ * const sleep = (pb, i) => {
8
+ * pb.update(i);
9
+ * return new Promise((res, rej) => setTimeout(res, ms));
10
+ * };
11
+ * for (let i=0; i<=total; i++) {
12
+ * await sleep(pb, i);
13
+ * }
14
+ */
15
+ class ProgressBar {
16
+ constructor(options = {
17
+ desc: "Progress",
18
+ total: 100
19
+ }) {
20
+ this.desc = options.desc
21
+ this.total = options.total
22
+ this.current = 0;
23
+ this.startTime = Date.now();
24
+ }
25
+
26
+ update(current) {
27
+ this.current = Math.min(current, this.total);
28
+ this.render();
29
+ }
30
+
31
+ render() {
32
+ const terminalWidth = process.stdout.columns || 80;
33
+
34
+ const percent = this.total === 0 ? 100 : (this.current / this.total) * 100;
35
+ const percentStr = percent.toFixed(1) + "%";
36
+
37
+ // calculate elapsed and estimated time
38
+ const elapsedTime = Date.now() - this.startTime;
39
+ const elapsedSec = Math.floor(elapsedTime / 1000);
40
+ const rate = this.current / (elapsedTime / 1000);
41
+ const remainingTime = rate > 0 ? Math.floor((this.total - this.current) / rate) : 0;
42
+
43
+ const timeStr = `${this.formatTime(elapsedSec)}/${this.formatTime(remainingTime)}`;
44
+ const countStr = `${this.current}/${this.total}`;
45
+
46
+ // calculate available space for bar
47
+ const prefix = `${this.desc} [`;
48
+ const suffix = `] ${percentStr} ${countStr} ${timeStr}`;
49
+ const availableWidth = terminalWidth - prefix.length - suffix.length;
50
+
51
+ if (availableWidth < 10) {
52
+ process.stdout.write(`\r${this.desc}: ${percentStr}`);
53
+ return;
54
+ }
55
+
56
+ // build progress bar
57
+ const filledWidth = Math.floor((this.current / this.total) * availableWidth);
58
+ const emptyWidth = availableWidth - filledWidth;
59
+ const bar = "█".repeat(filledWidth) + "░".repeat(emptyWidth);
60
+
61
+ // build and display complete line
62
+ const line = prefix + bar + suffix;
63
+ process.stdout.write("\r" + line);
64
+
65
+ // new line when complete
66
+ if (this.current === this.total) {
67
+ process.stdout.write("\n");
68
+ }
69
+ }
70
+
71
+ formatTime(seconds) {
72
+ const h = Math.floor(seconds / 3600);
73
+ const m = Math.floor((seconds % 3600) / 60);
74
+ const s = seconds % 60;
75
+
76
+ if (h > 0) {
77
+ return `${h}h${m}m`;
78
+ } else if (m > 0) {
79
+ return `${m}m${s}s`;
80
+ } else {
81
+ return `${s}s`;
82
+ }
83
+ }
84
+ }
85
+
86
+ export default ProgressBar
@@ -14,8 +14,15 @@ AIIINOTATE_HOST=127.0.0.1
14
14
  # HTTP scheme: HTTP or HTTPS. should be HTTP in dev and in docker
15
15
  AIIINOTATE_SCHEME=http
16
16
 
17
+ # where to output logs. either: "stdout"|"file"|"stdout+file"|"off"
18
+ AIIINOTATE_LOG_TARGET=stdout
19
+ # directory to save logs to (if AIIINOTATE_LOG_TARGET enables logging to file)
20
+ AIIINOTATE_LOG_DIR=
21
+
17
22
  # max number of items to display per result page
18
23
  AIIINOTATE_PAGE_SIZE=5000
24
+ # "true"|"false". strict error throwing when inserting annotations. equivalent to `throwOnCanvasIndexError=true` (raise an error if an annotation's target manifest can't be found) and `throwOnXywhError=true` (raise an error if you can't extract a bounding box for an annotation's target). use in controlled environments where you know precisely the structure of your annotations and where you know that your manifests are accessible through HTTP.
25
+ AIIINOTATE_STRICT_MODE=false
19
26
 
20
27
  # IGNORE
21
28
  AIIINOTATE_BASE_URL="$AIIINOTATE_SCHEME://$AIIINOTATE_HOST:$AIIINOTATE_PORT"