{"id":21458155,"url":"https://github.com/samuelselasi/alx-files_manager","last_synced_at":"2026-07-10T16:32:05.490Z","repository":{"id":210039809,"uuid":"725560286","full_name":"samuelselasi/alx-files_manager","owner":"samuelselasi","description":"A simple platform to upload and view files","archived":false,"fork":false,"pushed_at":"2023-12-06T18:30:32.000Z","size":81,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-09T16:41:33.103Z","etag":null,"topics":["authentication","backend","background-processing","pagination"],"latest_commit_sha":null,"homepage":"https://github.com/samuelselasi/alx-files_manager.git","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/samuelselasi.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-11-30T12:01:01.000Z","updated_at":"2023-12-30T11:30:54.000Z","dependencies_parsed_at":"2023-12-06T19:42:50.695Z","dependency_job_id":null,"html_url":"https://github.com/samuelselasi/alx-files_manager","commit_stats":null,"previous_names":["samuelselasi/alx-files_manager"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/samuelselasi/alx-files_manager","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/samuelselasi%2Falx-files_manager","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/samuelselasi%2Falx-files_manager/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/samuelselasi%2Falx-files_manager/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/samuelselasi%2Falx-files_manager/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/samuelselasi","download_url":"https://codeload.github.com/samuelselasi/alx-files_manager/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/samuelselasi%2Falx-files_manager/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35337280,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-10T02:00:06.465Z","response_time":60,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["authentication","backend","background-processing","pagination"],"created_at":"2024-11-23T06:17:59.348Z","updated_at":"2026-07-10T16:32:05.472Z","avatar_url":"https://github.com/samuelselasi.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 0x04. Files manager\n#### `Back-end` `JavaScript` `ES6` `NoSQL` `MongoDB` `Redis` `NodeJS` `ExpressJS` `Kue`\n\n![images](https://github.com/samuelselasi/alx-files_manager/assets/85158665/ba83f480-f7aa-4312-9681-f3810e246226)\n\nThis project is a summary of this back-end trimester: `authentication`, `NodeJS`, `MongoDB`, `Redis`, `pagination` and `background processing`.\n\nThe objective is to **build a simple platform to upload and view files**:\n\n* User authentication via a token\n* List all files\n* Upload a new file\n* Change permission of a file\n* View a file\n* Generate thumbnails for images\n\nYou will be guided step by step for building it, but you have some freedoms of implementation, split in more files etc… ([utils](./utils) folder will be your friend)\n\nOf course, this kind of service already exists in the real life - it’s a learning purpose to assemble each piece and build a full product.\n\nEnjoy!\n\n## Resources\n### Read or watch:\n\n* Node JS getting started\n* Process API doc\n* Express getting started\n* Mocha documentation\n* Nodemon documentation\n* MongoDB\n* Bull\n* Image thumbnail\n* Mime-Types\n* Redis\n\n## Requirements\n* Allowed editors: `vi`, `vim`, `emacs`, `Visual Studio Code`\n* All your files will be interpreted/compiled on Ubuntu `18.04 LTS` using `node` (version `12.x.x`)\n* All your files should end with a new line\n* A `README.md` file, at the root of the folder of the project, is mandatory\n* Your code should use the `js` extension\n* Your code will be verified against lint using `ESLint`\n\n## Provided files\n### `package.json`\n\u003cdetails\u003e\n\n\u003csummary\u003eClick to show/hide file contents\u003c/summary\u003e\n\n```\n{\n  \"name\": \"files_manager\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"lint\": \"./node_modules/.bin/eslint\",\n    \"check-lint\": \"lint [0-9]*.js\",\n    \"start-server\": \"nodemon --exec babel-node --presets @babel/preset-env ./server.js\",\n    \"start-worker\": \"nodemon --exec babel-node --presets @babel/preset-env ./worker.js\",\n    \"dev\": \"nodemon --exec babel-node --presets @babel/preset-env\",\n    \"test\": \"./node_modules/.bin/mocha --require @babel/register --exit\" \n  },\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"dependencies\": {\n    \"bull\": \"^3.16.0\",\n    \"chai-http\": \"^4.3.0\",\n    \"express\": \"^4.17.1\",\n    \"image-thumbnail\": \"^1.0.10\",\n    \"mime-types\": \"^2.1.27\",\n    \"mongodb\": \"^3.5.9\",\n    \"redis\": \"^2.8.0\",\n    \"sha1\": \"^1.1.1\",\n    \"uuid\": \"^8.2.0\"\n  },\n  \"devDependencies\": {\n    \"@babel/cli\": \"^7.8.0\",\n    \"@babel/core\": \"^7.8.0\",\n    \"@babel/node\": \"^7.8.0\",\n    \"@babel/preset-env\": \"^7.8.2\",\n    \"@babel/register\": \"^7.8.0\",\n    \"chai\": \"^4.2.0\",\n    \"chai-http\": \"^4.3.0\",\n    \"mocha\": \"^6.2.2\",\n    \"nodemon\": \"^2.0.2\",\n    \"eslint\": \"^6.4.0\",\n    \"eslint-config-airbnb-base\": \"^14.0.0\",\n    \"eslint-plugin-import\": \"^2.18.2\",\n    \"eslint-plugin-jest\": \"^22.17.0\",\n    \"request\": \"^2.88.0\",\n    \"sinon\": \"^7.5.0\"\n  }\n}\n```\n\n\u003c/details\u003e\n\n### `.eslintrc.js`\n\u003cdetails\u003e\n\n\u003csummary\u003eClick to show/hide file contents\u003c/summary\u003e\n\n```\nmodule.exports = {\n    env: {\n      browser: false,\n      es6: true,\n      jest: true,\n    },\n    extends: [\n      'airbnb-base',\n      'plugin:jest/all',\n    ],\n    globals: {\n      Atomics: 'readonly',\n      SharedArrayBuffer: 'readonly',\n    },\n    parserOptions: {\n      ecmaVersion: 2018,\n      sourceType: 'module',\n    },\n    plugins: ['jest'],\n    rules: {\n      'max-classes-per-file': 'off',\n      'no-underscore-dangle': 'off',\n      'no-console': 'off',\n      'no-shadow': 'off',\n      'no-restricted-syntax': [\n        'error',\n        'LabeledStatement',\n        'WithStatement',\n      ],\n    },\n    overrides:[\n      {\n        files: ['*.js'],\n        excludedFiles: 'babel.config.js',\n      }\n    ]\n};\n```\n\n\u003c/details\u003e\n\n### `babel.config.js`\n\u003cdetails\u003e\n\n\u003csummary\u003eClick to show/hide file contents\u003c/summary\u003e\n\n```\nmodule.exports = {\n    presets: [\n      [\n        '@babel/preset-env',\n        {\n          targets: {\n            node: 'current',\n          },\n        },\n      ],\n    ],\n};\n```\n\n\u003c/details\u003e\n\n### and...\nDon’t forget to run `$ npm install` when you have the `package.json`\n\n## Tasks\n\n[0. Redis utils](./utils/redis.js)\n\nInside the folder `utils`, create a file `redis.js` that contains the class `RedisClient`.\n\n`RedisClient` should have:\n\n* the constructor that creates a client to Redis:\n\t* any error of the redis client must be displayed in the console (you should use `on('error')` of the redis client)\n* a function `isAlive` that returns true when the connection to Redis is a success otherwise, `false`\n* an asynchronous function `get` that takes a string key as argument and returns the Redis value stored for this key\n* an asynchronous function `set` that takes a string key, a value and a duration in second as arguments to store it in Redis (with an expiration set by the duration argument)\n* an asynchronous function `del` that takes a string key as argument and remove the value in Redis for this key\n\nAfter the class definition, create and export an instance of `RedisClient` called `redisClient`.\n```\nbob@dylan:~$ cat main.js\nimport redisClient from './utils/redis';\n\n(async () =\u003e {\n    console.log(redisClient.isAlive());\n    console.log(await redisClient.get('myKey'));\n    await redisClient.set('myKey', 12, 5);\n    console.log(await redisClient.get('myKey'));\n\n    setTimeout(async () =\u003e {\n        console.log(await redisClient.get('myKey'));\n    }, 1000*10)\n})();\n\nbob@dylan:~$ npm run dev main.js\ntrue\nnull\n12\nnull\nbob@dylan:~$\n```\n\n[1. MongoDB utils](./utils/db.js)\n\nInside the folder `utils`, create a file `db.js` that contains the class `DBClient`.\n\n`DBClient` should have:\n\n* the constructor that creates a client to MongoDB:\n\t* host: from the environment variable `DB_HOST` or default: `localhost`\n\t* port: from the environment variable `DB_PORT` or default: `27017`\n\t* database: from the environment variable `DB_DATABASE` or default: `files_manager`\n* a function `isAlive` that returns `true` when the connection to MongoDB is a success otherwise, `false`\n* an asynchronous function `nbUsers` that returns the number of documents in the collection `users`\n* an asynchronous function `nbFiles` that returns the number of documents in the collection `files`\n\nAfter the class definition, create and export an instance of `DBClient` called `dbClient`.\n```\nbob@dylan:~$ cat main.js\nimport dbClient from './utils/db';\n\nconst waitConnection = () =\u003e {\n    return new Promise((resolve, reject) =\u003e {\n        let i = 0;\n        const repeatFct = async () =\u003e {\n            await setTimeout(() =\u003e {\n                i += 1;\n                if (i \u003e= 10) {\n                    reject()\n                }\n                else if(!dbClient.isAlive()) {\n                    repeatFct()\n                }\n                else {\n                    resolve()\n                }\n            }, 1000);\n        };\n        repeatFct();\n    })\n};\n\n(async () =\u003e {\n    console.log(dbClient.isAlive());\n    await waitConnection();\n    console.log(dbClient.isAlive());\n    console.log(await dbClient.nbUsers());\n    console.log(await dbClient.nbFiles());\n})();\n\nbob@dylan:~$ npm run dev main.js\nfalse\ntrue\n4\n30\nbob@dylan:~$ \n```\n\n[2. First API](./controllers/AppController.js)\n\nInside `server.js`, create the Express server:\n\n* it should listen on the port set by the environment variable `PORT` or by default 5000\n* it should load all routes from the file `routes/index.js`\nInside the folder `routes`, create a file `index.js` that contains all endpoints of our API:\n\n\t* `GET /status` =\u003e `AppController.getStatus`\n\t* `GET /stats` =\u003e `AppController.getStats`\nInside the folder `controllers`, create a file `AppController.js` that contains the definition of the 2 endpoints:\n\n* `GET /status` should return if Redis is alive and if the DB is alive too by using the 2 utils created previously: `{ \"redis\": true, \"db\": true }` with a status code 200\n* `GET /stats` should return the number of users and files in DB: `{ \"users\": 12, \"files\": 1231 }` with a status code 200\n\t* `users` collection must be used for counting all users\n\t* `files` collection must be used for counting all files\n**Terminal 1:**\n```\nbob@dylan:~$ npm run start-server\nServer running on port 5000\n...\n```\n**Terminal 2:**\n```\nbob@dylan:~$ curl 0.0.0.0:5000/status ; echo \"\"\n{\"redis\":true,\"db\":true}\nbob@dylan:~$ \nbob@dylan:~$ curl 0.0.0.0:5000/stats ; echo \"\"\n{\"users\":4,\"files\":30}\nbob@dylan:~$ \n```\n\n[3. Create a new user](./controllers/UsersController.js)\n\nNow that we have a simple API, it’s time to add users to our database.\n\nIn the file `routes/index.js`, add a new endpoint:\n\n\t* `POST /users` =\u003e `UsersController.postNew`\nInside `controllers`, add a file `UsersController.js` that contains the new endpoint:\n\n`POST /users` should create a new user in DB:\n\n* To create a user, you must specify an `email` and a `password`\n* If the `email` is missing, return an error `Missing email` with a status code 400\n* If the `password` is missing, return an error `Missing password` with a status code 400\n* If the `email` already exists in DB, return an error `Already exist` with a status code 400\n* The `password` must be stored after being hashed in `SHA1`\n* The endpoint is returning the new user with only the `email` and the `id` (auto generated by MongoDB) with a status code 201\n* The new user must be saved in the collection `users`:\n\t* `email`: same as the value received\n\t* `password`: `SHA1` value of the value received\n```\nbob@dylan:~$ curl 0.0.0.0:5000/users -XPOST -H \"Content-Type: application/json\" -d '{ \"email\": \"bob@dylan.com\", \"password\": \"toto1234!\" }' ; echo \"\"\n{\"id\":\"5f1e7d35c7ba06511e683b21\",\"email\":\"bob@dylan.com\"}\nbob@dylan:~$ \nbob@dylan:~$ echo 'db.users.find()' | mongo files_manager\n{ \"_id\" : ObjectId(\"5f1e7d35c7ba06511e683b21\"), \"email\" : \"bob@dylan.com\", \"password\" : \"89cad29e3ebc1035b29b1478a8e70854f25fa2b2\" }\nbob@dylan:~$ \nbob@dylan:~$ \nbob@dylan:~$ curl 0.0.0.0:5000/users -XPOST -H \"Content-Type: application/json\" -d '{ \"email\": \"bob@dylan.com\", \"password\": \"toto1234!\" }' ; echo \"\"\n{\"error\":\"Already exist\"}\nbob@dylan:~$ \nbob@dylan:~$ curl 0.0.0.0:5000/users -XPOST -H \"Content-Type: application/json\" -d '{ \"email\": \"bob@dylan.com\" }' ; echo \"\"\n{\"error\":\"Missing password\"}\nbob@dylan:~$\n``` \n\n[4. Authenticate a user](./controllers/AuthController.js)\n\nIn the file `routes/index.js`, add `3` new endpoints:\n\n* `GET /connect =\u003e AuthController.getConnect`\n* `GET /disconnect =\u003e AuthController.getDisconnect`\n* `GET /users/me =\u003e UserController.getMe`\n\nInside `controllers`, add a file `AuthController.js` that contains new endpoints:\n\n`GET /connect` should sign-in the user by generating a new authentication token:\n\n* By using the header `Authorization` and the technique of the Basic auth (`Base64` of the `\u003cemail\u003e:\u003cpassword\u003e`), find the user associate to this `email` and with this `password` (**reminder**: we are storing the `SHA1` of the `password`)\n* If no `user` has been found, return an error `Unauthorized` with a status code `401`\n* **Otherwise**:\n\t* Generate a random string (using `uuidv4`) as token\n\t* Create a key: `auth_\u003ctoken\u003e`\n\t* Use this `key` for storing in Redis (by using the `redisClient` create previously) the user ID for `24` hours\n\t* Return this token: `{ \"token\": \"155342df-2399-41da-9e8c-458b6ac52a0c\" }` with a status code `200`\n\nNow, we have a way to identify a user, create a token (= avoid to store the password on any front-end) and use this token for 24h to access to the API!\n\nEvery authenticated endpoints of our API will look at this token inside the header `X-Token`.\n\n`GET /disconnect` should sign-out the `user` based on the `token`:\n\n* Retrieve the `user` based on the `token`:\n\t* If not found, return an error `Unauthorized` with a status code `401`\n\t* Otherwise, delete the token in Redis and return nothing with a status code `204`\n\nInside the file `controllers/UsersController.js` add a new endpoint:\n\n`GET /users/me` should retrieve the `user` base on the `token` used:\n\n* Retrieve the `user` based on the `token`:\n\t* If not found, return an error Unauthorized with a status code `401`\n\t* Otherwise, return the `user` object (`email` and `id` only)\n```\nbob@dylan:~$ curl 0.0.0.0:5000/connect -H \"Authorization: Basic Ym9iQGR5bGFuLmNvbTp0b3RvMTIzNCE=\" ; echo \"\"\n{\"token\":\"031bffac-3edc-4e51-aaae-1c121317da8a\"}\nbob@dylan:~$ \nbob@dylan:~$ curl 0.0.0.0:5000/users/me -H \"X-Token: 031bffac-3edc-4e51-aaae-1c121317da8a\" ; echo \"\"\n{\"id\":\"5f1e7cda04a394508232559d\",\"email\":\"bob@dylan.com\"}\nbob@dylan:~$ \nbob@dylan:~$ curl 0.0.0.0:5000/disconnect -H \"X-Token: 031bffac-3edc-4e51-aaae-1c121317da8a\" ; echo \"\"\n\nbob@dylan:~$ curl 0.0.0.0:5000/users/me -H \"X-Token: 031bffac-3edc-4e51-aaae-1c121317da8a\" ; echo \"\"\n{\"error\":\"Unauthorized\"}\nbob@dylan:~$\n```\n\n[5. First file](./controllers/FilesController.js)\n\nIn the file [routes/index.js](./routes/index.js), add a new endpoint:\n\n* `POST /files` =\u003e `FilesController.postUpload`\n\nInside `controllers`, add a file `FilesController.js` that contains the new endpoint:\n\n`POST /files` should create a new file in DB and in disk:\n\n* Retrieve the user based on the token:\n\t* If not found, return an error `Unauthorized` with a status code `401`\n* To create a file, you must specify:\n\t* `name`: as filename\n\t* `type`: either `folder`, `file` or `image`\n\t* `parentId`: (*optional*) as ID of the parent (**default**: `0` -\u003e the root)\n\t* `isPublic`: (*optional*) as boolean to define if the file is public or not (**default**: `false`)\n\t* `data`: (only for `type=file|image`) as Base64 of the file content\n* If the `name` is missing, return an error `Missing name` with a status code `400`\n* If the `type` is missing or not part of the list of accepted type, return an error `Missing type` with a status code `400`\n* If the `data` is missing and `type != folder`, return an error `Missing data` with a status code `400`\n* If the `parentId` is set:\n\t* If no file is present in DB for this `parentId`, return an error `Parent not found` with a status code `400`\n\t* If the file present in DB for this `parentId` is not of type `folder`, return an error `Parent is not a folder` with a status code `400`\n* The user ID should be added to the document saved in DB - as owner of a file\n* If the type is `folder`, add the new file document in the DB and return the new file with a status code `201`\n* **Otherwise**:\n\t* All file will be stored locally in a folder (to create automatically if not present):\n\t\t* The relative path of this folder is given by the environment variable `FOLDER_PATH`\n\t\t* If this variable is not present or empty, use `/tmp/files_manager` as storing folder path\n\t* Create a local path in the storing folder with filename a UUID\n\t* Store the file in clear (**reminder**: `data` contains the Base64 of the file) in this local path\n\t* Add the new file document in the collection `files` with these attributes:\n\t\t* `userId`: ID of the owner document (owner from the authentication)\n\t\t* `name`: same as the value received\n\t\t* `type`: same as the value received\n\t\t* `isPublic`: same as the value received\n\t\t* `parentId`: same as the value received - if not present: `0`\n\t\t* `localPath`: for a `type=file|image`, the absolute path to the file save in local\n\t* Return the new file with a status code `201`\n\n```\nbob@dylan:~$ curl 0.0.0.0:5000/connect -H \"Authorization: Basic Ym9iQGR5bGFuLmNvbTp0b3RvMTIzNCE=\" ; echo \"\"\n{\"token\":\"f21fb953-16f9-46ed-8d9c-84c6450ec80f\"}\nbob@dylan:~$ \nbob@dylan:~$ curl -XPOST 0.0.0.0:5000/files -H \"X-Token: f21fb953-16f9-46ed-8d9c-84c6450ec80f\" -H \"Content-Type: application/json\" -d '{ \"name\": \"myText.txt\", \"type\": \"file\", \"data\": \"SGVsbG8gV2Vic3RhY2shCg==\" }' ; echo \"\"\n{\"id\":\"5f1e879ec7ba06511e683b22\",\"userId\":\"5f1e7cda04a394508232559d\",\"name\":\"myText.txt\",\"type\":\"file\",\"isPublic\":false,\"parentId\":0}\nbob@dylan:~$\nbob@dylan:~$ ls /tmp/files_manager/\n2a1f4fc3-687b-491a-a3d2-5808a02942c9\nbob@dylan:~$\nbob@dylan:~$ cat /tmp/files_manager/2a1f4fc3-687b-491a-a3d2-5808a02942c9 \nHello Webstack!\nbob@dylan:~$\nbob@dylan:~$ curl -XPOST 0.0.0.0:5000/files -H \"X-Token: f21fb953-16f9-46ed-8d9c-84c6450ec80f\" -H \"Content-Type: application/json\" -d '{ \"name\": \"images\", \"type\": \"folder\" }' ; echo \"\"\n{\"id\":\"5f1e881cc7ba06511e683b23\",\"userId\":\"5f1e7cda04a394508232559d\",\"name\":\"images\",\"type\":\"folder\",\"isPublic\":false,\"parentId\":0}\nbob@dylan:~$\nbob@dylan:~$ cat image_upload.py\nimport base64\nimport requests\nimport sys\n\nfile_path = sys.argv[1]\nfile_name = file_path.split('/')[-1]\n\nfile_encoded = None\nwith open(file_path, \"rb\") as image_file:\n    file_encoded = base64.b64encode(image_file.read()).decode('utf-8')\n\nr_json = { 'name': file_name, 'type': 'image', 'isPublic': True, 'data': file_encoded, 'parentId': sys.argv[3] }\nr_headers = { 'X-Token': sys.argv[2] }\n\nr = requests.post(\"http://0.0.0.0:5000/files\", json=r_json, headers=r_headers)\nprint(r.json())\n\nbob@dylan:~$\nbob@dylan:~$ python image_upload.py image.png f21fb953-16f9-46ed-8d9c-84c6450ec80f 5f1e881cc7ba06511e683b23\n{'id': '5f1e8896c7ba06511e683b25', 'userId': '5f1e7cda04a394508232559d', 'name': 'image.png', 'type': 'image', 'isPublic': True, 'parentId': '5f1e881cc7ba06511e683b23'}\nbob@dylan:~$\nbob@dylan:~$ echo 'db.files.find()' | mongo files_manager\n{ \"_id\" : ObjectId(\"5f1e881cc7ba06511e683b23\"), \"userId\" : ObjectId(\"5f1e7cda04a394508232559d\"), \"name\" : \"images\", \"type\" : \"folder\", \"parentId\" : \"0\" }\n{ \"_id\" : ObjectId(\"5f1e879ec7ba06511e683b22\"), \"userId\" : ObjectId(\"5f1e7cda04a394508232559d\"), \"name\" : \"myText.txt\", \"type\" : \"file\", \"parentId\" : \"0\", \"isPublic\" : false, \"localPath\" : \"/tmp/files_manager/2a1f4fc3-687b-491a-a3d2-5808a02942c9\" }\n{ \"_id\" : ObjectId(\"5f1e8896c7ba06511e683b25\"), \"userId\" : ObjectId(\"5f1e7cda04a394508232559d\"), \"name\" : \"image.png\", \"type\" : \"image\", \"parentId\" : ObjectId(\"5f1e881cc7ba06511e683b23\"), \"isPublic\" : true, \"localPath\" : \"/tmp/files_manager/51997b88-5c42-42c2-901e-e7f4e71bdc47\" }\nbob@dylan:~$\nbob@dylan:~$ ls /tmp/files_manager/\n2a1f4fc3-687b-491a-a3d2-5808a02942c9   51997b88-5c42-42c2-901e-e7f4e71bdc47\nbob@dylan:~$\n```\n\n[6. Get and list file](./controllers/FilesController.js)\n\nIn the file [routes/index.js](./routes/index.js), add `2` new endpoints:\n\n* `GET /files/:id` =\u003e `FilesController.getShow`\n* `GET /files` =\u003e `FilesController.getIndex`\n\nIn the file `controllers/FilesController.js`, add the `2` new endpoints:\n\n`GET /files/:id` should retrieve the file document based on the ID:\n\n* Retrieve the `user` based on the `token`:\n\t* If not found, return an error `Unauthorized` with a status code `401`\n* If no file document is linked to the user and the ID passed as parameter, return an error `Not found` with a status code `404`\n* Otherwise, return the file document\n\n`GET /files` should retrieve all users file documents for a specific `parentId` and with `pagination`:\n\n* Retrieve the `user` based on the `token`:\n\t* If not found, return an error `Unauthorized` with a status code `401`\n* Based on the query parameters `parentId` and `page`, return the list of file document\n\t* **parentId**:\n\t\t* No validation of `parentId` needed - if the `parentId` is not linked to any `user` folder, returns an empty list\n\t\t* By default, `parentId` is equal to `0` = the root\n\t* **Pagination**:\n\t\t* Each page should be `20` items max\n\t\t* `page` query parameter starts at `0` for the first page. If equals to `1`, it means it’s the second page (*form the `20th` to the `40th`*), etc…\n\t\t* Pagination can be done directly by the `aggregate` of MongoDB\n\n```\nbob@dylan:~$ curl 0.0.0.0:5000/connect -H \"Authorization: Basic Ym9iQGR5bGFuLmNvbTp0b3RvMTIzNCE=\" ; echo \"\"\n{\"token\":\"f21fb953-16f9-46ed-8d9c-84c6450ec80f\"}\nbob@dylan:~$ \nbob@dylan:~$ curl -XGET 0.0.0.0:5000/files -H \"X-Token: f21fb953-16f9-46ed-8d9c-84c6450ec80f\" ; echo \"\"\n[{\"id\":\"5f1e879ec7ba06511e683b22\",\"userId\":\"5f1e7cda04a394508232559d\",\"name\":\"myText.txt\",\"type\":\"file\",\"isPublic\":false,\"parentId\":0},{\"id\":\"5f1e881cc7ba06511e683b23\",\"userId\":\"5f1e7cda04a394508232559d\",\"name\":\"images\",\"type\":\"folder\",\"isPublic\":false,\"parentId\":0},{\"id\":\"5f1e8896c7ba06511e683b25\",\"userId\":\"5f1e7cda04a394508232559d\",\"name\":\"image.png\",\"type\":\"image\",\"isPublic\":true,\"parentId\":\"5f1e881cc7ba06511e683b23\"}]\nbob@dylan:~$\nbob@dylan:~$ curl -XGET 0.0.0.0:5000/files?parentId=5f1e881cc7ba06511e683b23 -H \"X-Token: f21fb953-16f9-46ed-8d9c-84c6450ec80f\" ; echo \"\"\n[{\"id\":\"5f1e8896c7ba06511e683b25\",\"userId\":\"5f1e7cda04a394508232559d\",\"name\":\"image.png\",\"type\":\"image\",\"isPublic\":true,\"parentId\":\"5f1e881cc7ba06511e683b23\"}]\nbob@dylan:~$\nbob@dylan:~$ curl -XGET 0.0.0.0:5000/files/5f1e8896c7ba06511e683b25 -H \"X-Token: f21fb953-16f9-46ed-8d9c-84c6450ec80f\" ; echo \"\"\n{\"id\":\"5f1e8896c7ba06511e683b25\",\"userId\":\"5f1e7cda04a394508232559d\",\"name\":\"image.png\",\"type\":\"image\",\"isPublic\":true,\"parentId\":\"5f1e881cc7ba06511e683b23\"}\nbob@dylan:~$\n```\n\n[7. File publish/unpublish](./controllers/FilesController.js)\n\nIn the file `routes/index.js`, add 2 new endpoints:\n\n* `PUT /files/:id/publish` =\u003e `FilesController.putPublish`\n* `PUT /files/:id/publish` =\u003e `FilesController.putUnpublish`\n\nIn the file `controllers/FilesController.js`, add the 2 new endpoints:\n\n`PUT /files/:id/publish` should set `isPublic` to `true` on the file document based on the ID:\n\n* Retrieve the user based on the token:\n\t* If not found, return an error `Unauthorized` with a status code 401\n* If no file document is linked to the user and the ID passed as parameter, return an error `Not found` with a status code 404\n* Otherwise:\n\t* Update the value of `isPublic` to `true`\n\t* And return the file document with a status code 200\n\n`PUT /files/:id/unpublish` should set `isPublic` to `false` on the file document based on the ID:\n\n* Retrieve the user based on the token:\n\t* If not found, return an error `Unauthorized` with a status code 401\n* If no file document is linked to the user and the ID passed as parameter, return an error `Not found` with a status code 404\n* Otherwise:\n\t* Update the value of `isPublic` to `false`\n\t* And return the file document with a status code 200\n\n```\nbob@dylan:~$ curl 0.0.0.0:5000/connect -H \"Authorization: Basic Ym9iQGR5bGFuLmNvbTp0b3RvMTIzNCE=\" ; echo \"\"\n{\"token\":\"f21fb953-16f9-46ed-8d9c-84c6450ec80f\"}\nbob@dylan:~$ \nbob@dylan:~$ curl -XGET 0.0.0.0:5000/files/5f1e8896c7ba06511e683b25 -H \"X-Token: f21fb953-16f9-46ed-8d9c-84c6450ec80f\" ; echo \"\"\n{\"id\":\"5f1e8896c7ba06511e683b25\",\"userId\":\"5f1e7cda04a394508232559d\",\"name\":\"image.png\",\"type\":\"image\",\"isPublic\":false,\"parentId\":\"5f1e881cc7ba06511e683b23\"}\nbob@dylan:~$\nbob@dylan:~$ curl -XPUT 0.0.0.0:5000/files/5f1e8896c7ba06511e683b25/publish -H \"X-Token: f21fb953-16f9-46ed-8d9c-84c6450ec80f\" ; echo \"\"\n{\"id\":\"5f1e8896c7ba06511e683b25\",\"userId\":\"5f1e7cda04a394508232559d\",\"name\":\"image.png\",\"type\":\"image\",\"isPublic\":true,\"parentId\":\"5f1e881cc7ba06511e683b23\"}\nbob@dylan:~$ \nbob@dylan:~$ curl -XPUT 0.0.0.0:5000/files/5f1e8896c7ba06511e683b25/unpublish -H \"X-Token: f21fb953-16f9-46ed-8d9c-84c6450ec80f\" ; echo \"\"\n{\"id\":\"5f1e8896c7ba06511e683b25\",\"userId\":\"5f1e7cda04a394508232559d\",\"name\":\"image.png\",\"type\":\"image\",\"isPublic\":false,\"parentId\":\"5f1e881cc7ba06511e683b23\"}\nbob@dylan:~$\n```\n\n[8. File data](./controllers/FilesController.js)\n\nIn the file `routes/index.js`, add one new endpoint:\n\n* `GET /files/:id/data =\u003e FilesController.getFile`\n\nIn the file `controllers/FilesController.js`, add the new endpoint:\n\n`GET /files/:id/data` should return the content of the file document based on the ID:\n\n* If no file document is linked to the ID passed as parameter, return an error `Not found` with a status code 404\n* If the file document (folder or file) is not public (`isPublic: false`) and no user authenticate or not the owner of the file, return an error `Not found` with a status code 404\n* If the type of the file document is `folder`, return an error `A folder doesn't have content` with a status code 400\n* If the file is not locally present, return an error `Not found` with a status code 404\n* Otherwise:\n\t* By using the module `mime-types`, get the [MIME-type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) based on the `name` of the file\n\t* Return the content of the file with the correct MIME-type\n\n```\nbob@dylan:~$ curl 0.0.0.0:5000/connect -H \"Authorization: Basic Ym9iQGR5bGFuLmNvbTp0b3RvMTIzNCE=\" ; echo \"\"\n{\"token\":\"f21fb953-16f9-46ed-8d9c-84c6450ec80f\"}\nbob@dylan:~$ \nbob@dylan:~$ curl -XPUT 0.0.0.0:5000/files/5f1e879ec7ba06511e683b22/unpublish -H \"X-Token: f21fb953-16f9-46ed-8d9c-84c6450ec80f\" ; echo \"\"\n{\"id\":\"5f1e879ec7ba06511e683b22\",\"userId\":\"5f1e7cda04a394508232559d\",\"name\":\"myText.txt\",\"type\":\"file\",\"isPublic\":false,\"parentId\":0}\nbob@dylan:~$ \nbob@dylan:~$ curl -XGET 0.0.0.0:5000/files/5f1e879ec7ba06511e683b22/data -H \"X-Token: f21fb953-16f9-46ed-8d9c-84c6450ec80f\" ; echo \"\"\nHello Webstack!\n\nbob@dylan:~$ curl -XGET 0.0.0.0:5000/files/5f1e879ec7ba06511e683b22/data ; echo \"\"\n{\"error\":\"Not found\"}\nbob@dylan:~$ \nbob@dylan:~$ curl -XPUT 0.0.0.0:5000/files/5f1e879ec7ba06511e683b22/publish -H \"X-Token: f21fb953-16f9-46ed-8d9c-84c6450ec80f\" ; echo \"\"\n{\"id\":\"5f1e879ec7ba06511e683b22\",\"userId\":\"5f1e7cda04a394508232559d\",\"name\":\"myText.txt\",\"type\":\"file\",\"isPublic\":true,\"parentId\":0}\nbob@dylan:~$ \nbob@dylan:~$ curl -XGET 0.0.0.0:5000/files/5f1e879ec7ba06511e683b22/data ; echo \"\"\nHello Webstack!\n\nbob@dylan:~$\n```\n\n[9. Image Thumbnails](./worker.js)\n\nUpdate the endpoint `POST /files` endpoint to start a background processing for generating thumbnails for a file of type `image`:\n\n* Create a `Bull` queue `fileQueue`\n* When a new image is stored (in local and in DB), add a job to this queue with the `userId` and `fileId`\n\nCreate a file `worker.js`:\n\n* By using the module `Bull`, create a queue `fileQueue`\n* Process this queue:\n\t* If `fileId` is not present in the job, raise an error `Missing fileId`\n\t* If `userId` is not present in the job, raise an error `Missing userId`\n\t* If no document is found in DB based on the `fileId` and `userId`, raise an error `File not found`\n\t* By using the module `image-thumbnail`, generate 3 thumbnails with `width` = 500, 250 and 100 - store each result on the same location of the original file by appending `_\u003cwidth size\u003e`\n\nUpdate the endpoint `GET /files/:id/data` to accept a query parameter `size`:\n\n* `size` can be `500`, `250` or `100`\n* Based on `size`, return the correct local file\n* If the local file doesn’t exist, return an error `Not found` with a status code 404\n\n**Terminal 3:** (start the worker)\n```\nbob@dylan:~$ npm run start-worker\n...\n```\n**Terminal 2:**\n```\nbob@dylan:~$ curl 0.0.0.0:5000/connect -H \"Authorization: Basic Ym9iQGR5bGFuLmNvbTp0b3RvMTIzNCE=\" ; echo \"\"\n{\"token\":\"f21fb953-16f9-46ed-8d9c-84c6450ec80f\"}\nbob@dylan:~$ \nbob@dylan:~$ python image_upload.py image.png f21fb953-16f9-46ed-8d9c-84c6450ec80f 5f1e881cc7ba06511e683b23\n{'id': '5f1e8896c7ba06511e683b25', 'userId': '5f1e7cda04a394508232559d', 'name': 'image.png', 'type': 'image', 'isPublic': True, 'parentId': '5f1e881cc7ba06511e683b23'}\nbob@dylan:~$ ls /tmp/files_manager/\n2a1f4fc3-687b-491a-a3d2-5808a02942c9   51997b88-5c42-42c2-901e-e7f4e71bdc47   6dc53397-8491-4b7c-8273-f748b1a031cb   6dc53397-8491-4b7c-8273-f748b1a031cb_100   6dc53397-8491-4b7c-8273-f748b1a031cb_250    6dc53397-8491-4b7c-8273-f748b1a031cb_500\nbob@dylan:~$ \nbob@dylan:~$ curl -XGET 0.0.0.0:5000/files/5f1e8896c7ba06511e683b25/data -so new_image.png ; file new_image.png\nnew_image.png: PNG image data, 471 x 512, 8-bit/color RGBA, non-interlaced\nbob@dylan:~$ \nbob@dylan:~$ curl -XGET 0.0.0.0:5000/files/5f1e8896c7ba06511e683b25/data?size=100 -so new_image.png ; file new_image.png\nnew_image.png: PNG image data, 100 x 109, 8-bit/color RGBA, non-interlaced\nbob@dylan:~$ \nbob@dylan:~$ curl -XGET 0.0.0.0:5000/files/5f1e8896c7ba06511e683b25/data?size=250 -so new_image.png ; file new_image.png\nnew_image.png: PNG image data, 250 x 272, 8-bit/color RGBA, non-interlaced\nbob@dylan:~$\n```\n\n[10. Tests!](./tests/)\n\nOf course, a strong and stable project can not be good without tests.\n\nCreate tests for `redisClient` and `dbClient`.\n\nCreate tests for each endpoints:\n\n* `GET /status`\n* `GET /stats`\n* `POST /users`\n* `GET /connect`\n* `GET /disconnect`\n* `GET /users/me`\n* `POST /files`\n* `GET /files/:id`\n* `GET /files` (don’t forget the pagination)\n* `PUT /files/:id/publish`\n* `PUT /files/:id/unpublish`\n* `GET /files/:id/data`\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsamuelselasi%2Falx-files_manager","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsamuelselasi%2Falx-files_manager","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsamuelselasi%2Falx-files_manager/lists"}