From bafcb5e7dd22d8a99b32d59a12cbdd07f470db81 Mon Sep 17 00:00:00 2001 From: ccornali611 Date: Thu, 14 Mar 2024 16:38:36 -0600 Subject: [PATCH 01/24] Added: files repository --- src/db/repositories/fileRepository.js | 185 ++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 src/db/repositories/fileRepository.js diff --git a/src/db/repositories/fileRepository.js b/src/db/repositories/fileRepository.js new file mode 100644 index 0000000..15f1fe7 --- /dev/null +++ b/src/db/repositories/fileRepository.js @@ -0,0 +1,185 @@ +/** + * Represents a repository for managing files in the database + */ +class FileRepository { + constructor (database) { + this.db = database + this.createTable() + } + + /** + * Creates the "files" table in the database if it does not exist + */ + createTable () { + this.db.prepare(` + CREATE TABLE IF NOT EXISTS files ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + description TEXT NOT NULL, + filename TEXT NOT NULL, + mimetype TEXT NOT NULL, + src TEXT NOT NULL, + username TEXT NOT NULL + ) + `).run() + // this.db.prepare('ADD COLUMN IF NOT EXISTS') + } + + /** + * Inserts a single file record into the database + * @param {Object} file - An object containing description, filename, mimetype, src, and username + * @returns {Object} The inserted file record + */ + insertFile ({ description, filename, mimetype, src, username }) { + const result = this.db + .prepare(` + INSERT INTO files (description, filename, mimetype, src, username) + VALUES (@description, @filename, @mimetype, @src, @username) + RETURNING * + `) + .run({ description, filename, mimetype, src, username }) + const dbFile = this.getFileById(result.lastInsertRowid) + // console.log(`[INFO] FileRepository.insertFile(): "${filename}"`, dbFile) + return dbFile + } + + /** + * Inserts multiple file records into the database in a single transaction. + * @param {Array} data - An array of objects (filename, descriptionm mimetype, src, and username) to be inserted into the database. + */ + bulkInsertFiles (data) { + const insert = this.db.prepare(` + INSERT INTO files + (description, filename, mimetype, src, username) + VALUES + (@description, @filename, @mimetype, @src, @username) + `) + + const insertMany = this.db.transaction((cats) => { + for (const cat of cats) { + insert.run({ + description: cat.description, + filename: cat.filename, + mimetype: cat.mimetype, + src: cat.src, + username: cat.username, + }) + } + }) + insertMany(data) + } + + /** + * Retrieves a file record from the database by its ID. + * @param {number} id - The ID of the file record. + * @returns {Object} The file record matching the provided ID. + */ + getFileById (id) { + return this.db.prepare(` + SELECT * FROM files + WHERE id = :id + `).get({ id }) + } + + /** + * Retrieves the count of files associated with a specific user. + * @param {string} username - The username of the user. + * @returns {number} The count of files uploaded by the user. + */ + getUserFilesCount (username) { + return this.db.prepare(` + SELECT COUNT(*) AS count FROM files + WHERE username = :username + `).get({ username }).count + } + + /** + * Retrieves paginated files associated with a specific user. + * @param {string} username - The username of the user. + * @returns {object} An object of file records uploaded by the user and total. + */ + getUserFiles ({ username }) { + return this.db.prepare(` + SELECT * FROM files + WHERE username = :username + `).all({ username }) + } + + /** + * Retrieves the count of all files. + * @returns {number} The count of all files. + */ + getTotal () { + return this.db.prepare(` + SELECT COUNT(*) AS count FROM files + `).get().count + } + + /** + * Retrieves all files stored in the database. + * @returns {Array} An array of all file records stored. + * @returns {Array} An array of all file records. + */ + getAllFiles () { + return this.db.prepare(`SELECT * FROM files`).all() + } + + /** + * Deletes a file record from the database by its ID. + * @param {number} id - The ID of the file record to be deleted. + * @returns {boolean} A boolean, return true if deletion was successful. + */ + deleteFileById (id) { + const deleteStmt = this.db.prepare(` + DELETE FROM files + WHERE id = :id + `) + return deleteStmt.run({ id }).changes > 0 + } + + /** + * Retrieves the original file record matching the provided criteria. + * @param {Object} criteria - An object match the containing (username, filename, and base64) for finding the original file. + * @returns {Object|null} The original file record matching the provided criteria or null if not found. + */ + getOriginalFile ({ username, filename, base64 }) { + return this.db.prepare(` + SELECT * + FROM files + WHERE + filename == :filename + AND src == :src + AND username == :username + `) + .get({ + filename, + username, + src: base64, + }) + } + + // getCountMatchingCriteria + /** + * Retrieves all files similar to the provided criteria. Used to get the list of duplicate document + * @param {Object} criteria - An object containing the criteria (username, filename, fileExt, and base64) for finding similar files. + * @returns {Array} An object with records similar to the provided criteria and total matching in DB. + */ + findAllLike ({ username, filename, fileExt, base64 }) { + return this.db.prepare(` + SELECT * + FROM files + WHERE + filename == :filename OR filename LIKE :similarFilename + AND src == :src + AND username == :username + ORDER BY filename ASC + `) + .all({ + filename: `${filename}.${fileExt}`, + similarFilename: `${filename}(%.${fileExt}`, + src: base64, + username, + }) + } +} + +module.exports = FileRepository \ No newline at end of file From 9d37ae7bbc7800d075a3ea2912f9f308d5654cf1 Mon Sep 17 00:00:00 2001 From: ccornali611 Date: Thu, 14 Mar 2024 16:39:40 -0600 Subject: [PATCH 02/24] Updated: seed data, added username --- scripts/seed.json | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/scripts/seed.json b/scripts/seed.json index 6ffc481..aec2181 100644 --- a/scripts/seed.json +++ b/scripts/seed.json @@ -4,30 +4,35 @@ "description": "A kitten that is 200x300", "filename": "kitten.jpg", "mimetype": "image/jpg", - "src": "http://placekitten.com/200/300" + "src": "http://placekitten.com/200/300", + "username": "testuser" }, { "id": "6eb00541-1fd5-4779-a155-ba0e53e0fdef", "description": "Another kitten, 500x300", "filename": "kitten(1).jpg", "mimetype": "image/jpg", - "src": "http://placekitten.com/500/300" + "src": "http://placekitten.com/500/300", + "username": "testuser" }, { "id": "6eb00541-1fd5-4779-a155-ba0e53e0f123", "description": "The last kitten. Black & White because 🎨 (800x700)", "filename": "kitten(2).jpg", "mimetype": "image/jpg", - "src": "http://placekitten.com/g/800/700" + "src": "http://placekitten.com/g/800/700", + "username": "testuser" }, { "id": "6eb00541-1fd5-4779-a155-ba0e53e0f12a", "description": "A fluffy dog. Way too much fur for that beach", "filename": "dog(2).jpg", "mimetype": "image/jpg", - "src": "https://placedog.net/1200/550" + "src": "https://placedog.net/1200/550", + "username": "testuser" }, { "id": "6eb00541-1fd5-4779-a155-ba0e53e0f0c0", "description": "The last known picture of a Elvis (probably)", "filename": "elvis.jpg", "mimetype": "image/jpeg", + "username": "testuser", "src": "data:image/jpeg;base64,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" }] } From f0947617e17304ebda7a62dbf0b8e9f6be5185ef Mon Sep 17 00:00:00 2001 From: ccornali611 Date: Thu, 14 Mar 2024 16:40:30 -0600 Subject: [PATCH 03/24] Updated: seed script to use FileRepository methods --- scripts/create-seed-db.js | 32 +++++++------------------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/scripts/create-seed-db.js b/scripts/create-seed-db.js index 8256e1d..c6e001a 100644 --- a/scripts/create-seed-db.js +++ b/scripts/create-seed-db.js @@ -2,6 +2,7 @@ const fs = require('fs') const path = require('path') const Database = require('better-sqlite3') const seedData = require('./seed.json') +const FileRepository = require('../src/db/repositories/fileRepository') const SEED_PATH = path.join(__dirname, '..', 'src', 'db', 'seed.db') @@ -12,34 +13,15 @@ function main () { } const db = new Database(SEED_PATH) - createTables(db) - addSeedData(db) + const fileRepository = new FileRepository(db) + fileRepository.createTable() + addSeedData(fileRepository) } -function createTables (db) { - console.log('Creating files table') - db.prepare(` - CREATE TABLE files ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - description TEXT not null, - filename TEXT not null, - mimetype TEXT not null, - src TEXT not null - ) - `).run() -} - -function addSeedData (db) { - console.log('Inserting seed data') - const insertFiles = db.prepare(` - INSERT INTO files - (description, filename, mimetype, src) - VALUES - (@description, @filename, @mimetype, @src) - `) - +function addSeedData (fileRepository) { + console.log('Inserting seed data'); for (const file of seedData.files) { - insertFiles.run(file) + fileRepository.insertFile(file.description, file.filename, file.mimetype, file.src, file.username); } } From e4fa0748146cb0cf145d644d857339ccd3947803 Mon Sep 17 00:00:00 2001 From: ccornali611 Date: Thu, 14 Mar 2024 16:42:31 -0600 Subject: [PATCH 04/24] Added: file upload helper --- src/utils/fileUploader.js | 115 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 src/utils/fileUploader.js diff --git a/src/utils/fileUploader.js b/src/utils/fileUploader.js new file mode 100644 index 0000000..12e5d37 --- /dev/null +++ b/src/utils/fileUploader.js @@ -0,0 +1,115 @@ +/** + * A utility class for handling file uploads. + */ +class FileUploadHelper { + /** + * Initializes the FileUploadHelper with file data. + * @param {Object} file - The file data object. + */ + constructor (file) { + /** + * The file data object. + * @type {Object} + */ + this.fileData = file + /** + * The original filename of the file. + * @type {string} + */ + this.originalFilename = file.name + + this.fileExt = null + this.fileBaseName = null + this.setFilenameParts() + } + + /** + * Parses the filename string to return the base filename and the file extension. + * @param {string} filename - The filename to parse. + * @returns {Object} An object containing the base filename and file extension. + */ + extractFilenameParts (filename) { + const fileNameParts = filename.split('.') + let baseFileName + + const fileExt = fileNameParts[fileNameParts.length - 1] + if (fileNameParts.length > 2) { + baseFileName = fileNameParts.slice(0, -1).join('.') + } else { + baseFileName = fileNameParts[0] + } + return { + baseFileName, + fileExt, + } + } + + /** + * Sets values for the class's base filename and the file extension + */ + setFilenameParts () { + const { fileExt, baseFileName } = this.extractFilenameParts(this.fileData.name) + this.fileExt = fileExt + this.fileBaseName = baseFileName + } + + /** + * Checks if the provided database file has the same name and source data as the current file. + * @param {Object} dbFile - The file data from the database. + * @returns {boolean} Returns true if the files are duplicate, otherwise false. + */ + isDuplicate (dbFile) { + return this.fileData.name === dbFile.filename && this.fileData.base64 === dbFile.src + } + + /** + * Generates a new filename for a duplicate file. + * If the filename already exists in the database, it appends a duplicate number to the filename. + * If the filename already contains a duplicate number, it replaces it with the new duplicate number. + * @param {string} dbFilename - The filename from the database. + * @param {number} dupNum - The duplicate number to append to the filename. + * @returns {string} The new filename. + */ + generateDuplicateFilename (dbFilename, dupNum) { + // If duplicate number is not provided, default to 1 + dupNum = dupNum || 1 + + const { baseFileName: dbBaseName, fileExt: dbFileExt } = this.extractFilenameParts(dbFilename) + // if the file to be uploaded has the same file name as entity in database, adding duplicate name syntax + if (dbFilename === this.originalFilename) { + return `${dbBaseName}(${dupNum}).${dbFileExt}` + } + + // For uploading files with names like 'test(1).jpg' or 'test(1)(1).jpg', handle duplicate name syntax + const filenamePattern = /^(?[\w\-_\d]+(\(\d+\))*?)(?=(?((\(\d+\))?\.[a-z]+$)))/ + let newFilename = dbFilename + + const matchResult = newFilename.match(filenamePattern) + if (matchResult) { + /** + * Extract base filename and extension from the matched groups + * Files like: + * - test.png will have a groups.name "test" and groups.end ".png" + * - test(1).png have a group.name "test" and groups.end "(1).png", + */ + newFilename = matchResult.groups.name + const extension = matchResult.groups.end + + // Extract the duplicate number from the ending using regex pattern + const dupNumPattern = /\d+/ + const dupNumMatch = extension.match(dupNumPattern) + + // If the duplicate number is found in the ending, replace it with the new duplicate number + if (dupNumMatch) { + extension.replace(dupNumMatch[0], dupNum.toString()) + newFilename += extension.replace(dupNumMatch[0], dupNum.toString()) + } else { + // If no duplicate number found, append the new duplicate number to the filename + newFilename += `(${dupNum.toString()})${extension}` + } + } + return newFilename + } +} + +module.exports = FileUploadHelper \ No newline at end of file From be5ca6d873bec5c5b8bb9ae81a0bda11c8dd92f4 Mon Sep 17 00:00:00 2001 From: ccornali611 Date: Thu, 14 Mar 2024 16:43:42 -0600 Subject: [PATCH 05/24] Added: middleware to send username in request --- src/server/index.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/server/index.js b/src/server/index.js index 084533e..5ee876d 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -1,4 +1,5 @@ const path = require('path') +const os = require('os') const express = require('express') const appModulePath = require('app-module-path') @@ -7,6 +8,12 @@ appModulePath.addPath(path.join(__dirname, '..', '..', 'src')) const routes = require('./routes') const app = express() +// Middleware to attach username to request object +app.use((req, res, next) => { + req.username = os.userInfo().username + next() +}) + app.use(express.static('dist')) app.use(express.json({ inflate: true, From 42c0a471c9698b0ee2f1aebd663e1ac25179d6a1 Mon Sep 17 00:00:00 2001 From: ccornali611 Date: Thu, 14 Mar 2024 16:47:50 -0600 Subject: [PATCH 06/24] Updated: get username endpoint to get username from request --- src/server/routes/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/server/routes/index.js b/src/server/routes/index.js index 9da68c8..8b9c991 100644 --- a/src/server/routes/index.js +++ b/src/server/routes/index.js @@ -1,4 +1,3 @@ -const os = require('os') const db = require('db') // SQLite usage @@ -7,7 +6,7 @@ const db = require('db') function buildRoutes (router) { router.get('/api/username', async (req, res) => { - return res.send({ username: os.userInfo().username }) + return res.send({ username: req.username }) }) router.get('/api/files', async (req, res) => { From e3d075c4c96de10a804a894e856354e352708fde Mon Sep 17 00:00:00 2001 From: ccornali611 Date: Thu, 14 Mar 2024 17:23:37 -0600 Subject: [PATCH 07/24] Updated: impleted using FileRepository on endpoints --- src/server/routes/index.js | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/src/server/routes/index.js b/src/server/routes/index.js index 8b9c991..fdc6bdb 100644 --- a/src/server/routes/index.js +++ b/src/server/routes/index.js @@ -1,39 +1,33 @@ const db = require('db') +const FileRepository = require('db/repositories/fileRepository') // SQLite usage // https://github.com/WiseLibs/better-sqlite3 // https://github.com/WiseLibs/better-sqlite3/blob/master/docs/api.md function buildRoutes (router) { + const fileRepository = new FileRepository(db.instance) + router.get('/api/username', async (req, res) => { return res.send({ username: req.username }) }) router.get('/api/files', async (req, res) => { - const files = db.instance - .prepare(` - SELECT * FROM files - `) - .all() // use .get() to fetch a single row + const files = fileRepository.getUserFiles({ username: req.username }) return res.send(files) }) router.post('/api/files', async (req, res) => { + const username = req.username const { description, file } = req.body - const newFile = db.instance - .prepare(` - INSERT INTO files - (description, filename, mimetype, src) - VALUES - (@description, @filename, @mimetype, @src) - RETURNING * - `) - .get({ - description, - filename: file.name, - mimetype: file.mimetype, - src: file.base64, - }) + + const newFile = fileRepository.insertFile({ + description, + filename: file.name, + mimetype: file.mimetype, + src: file.base64, + username, + }) return res.send(newFile) }) From 9891d5df7418d97adcffb5455f0dab5bb2c2a977 Mon Sep 17 00:00:00 2001 From: ccornali611 Date: Thu, 14 Mar 2024 17:47:32 -0600 Subject: [PATCH 08/24] Updated: put file endpoint, use FileUploadHelper to generate duplicate filename --- src/server/routes/index.js | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/server/routes/index.js b/src/server/routes/index.js index fdc6bdb..f5c4f33 100644 --- a/src/server/routes/index.js +++ b/src/server/routes/index.js @@ -1,5 +1,6 @@ const db = require('db') const FileRepository = require('db/repositories/fileRepository') +const FileUploadHelper = require('utils/fileUploader') // SQLite usage // https://github.com/WiseLibs/better-sqlite3 @@ -21,9 +22,43 @@ function buildRoutes (router) { const username = req.username const { description, file } = req.body + const dbOriginalFile = fileRepository.getOriginalFile({ + username, + filename: file.name, + base64: file.base64, + }) + + let filename = file.name + + if (dbOriginalFile) { + const fileUploader = new FileUploadHelper(file) + const existingFiles = fileRepository.findAllLike({ + username, + filename: fileUploader.fileBaseName, + base64: file.base64, + fileExt: fileUploader.fileExt, + }) + + if (!existingFiles.length) { + filename = fileUploader.generateDuplicateFilename(dbOriginalFile.filename) + } else { + // If there are multiple duplicate db files, finding the next duplicate number (fill the gaps) + let i = 0 + while (i < existingFiles.length - 1) { + iters += 1 + // if current file name identifier does not match the current iteration, breakout out of loop. Missing a duplicate, gap should be filled + if (existingFiles[i].filename !== `${fileUploader.fileBaseName}(${i+1}).${fileUploader.fileExt}`) { + break + } + i += 1 + } + filename = fileUploader.generateDuplicateFilename(existingFiles[i].filename, i+1) + } + } + const newFile = fileRepository.insertFile({ description, - filename: file.name, + filename, mimetype: file.mimetype, src: file.base64, username, From a992bcdeadfd31c489528053819cf6407f961253 Mon Sep 17 00:00:00 2001 From: ccornali611 Date: Thu, 14 Mar 2024 18:08:24 -0600 Subject: [PATCH 09/24] Added: test data generation helper function --- test/server/routes/index.test.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/server/routes/index.test.js b/test/server/routes/index.test.js index 90e3a00..d8c8ab9 100644 --- a/test/server/routes/index.test.js +++ b/test/server/routes/index.test.js @@ -5,6 +5,26 @@ const buildMockRouter = require('../buildMockRouter') describe('routes', function () { let router, route, res, req + + const testBaseFileName = 'bobby-tables' + const testFileExtension = 'jpg' + + function buildUploadData ({ + baseFileName=null, + fileExt=null, + base64=null, + mimetype=null, + }) { + return { + description: 'A portait of an artist', + file: { + name: `${baseFileName || testBaseFileName}.${fileExt || testFileExtension}`, + mimetype: mimetype || 'image/jpg', + base64: base64 || 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==', + }, + } + } + beforeEach(async function () { req = {} res = { From fae6d52131250dca25679d1d4184c2a3c608257d Mon Sep 17 00:00:00 2001 From: ccornali611 Date: Thu, 14 Mar 2024 18:09:07 -0600 Subject: [PATCH 10/24] Added: test validation function --- test/server/routes/index.test.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/server/routes/index.test.js b/test/server/routes/index.test.js index d8c8ab9..5e49388 100644 --- a/test/server/routes/index.test.js +++ b/test/server/routes/index.test.js @@ -25,6 +25,14 @@ describe('routes', function () { } } + function validateTest ({ inputFile, expectedFilename, responseBody }) { + expect(responseBody.id).to.be.ok + expect(responseBody.description).to.equal(inputFile.description) + expect(responseBody.filename).to.equal(expectedFilename) + expect(responseBody.mimetype).to.equal(inputFile.file.mimetype) + expect(responseBody.src).to.equal(inputFile.file.base64) + } + beforeEach(async function () { req = {} res = { From 2956f0d076b6a70687f6e385c73c5099c6ba2ecc Mon Sep 17 00:00:00 2001 From: ccornali611 Date: Thu, 14 Mar 2024 18:14:04 -0600 Subject: [PATCH 11/24] Updated: tests to use helper functions --- test/server/routes/index.test.js | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/test/server/routes/index.test.js b/test/server/routes/index.test.js index 5e49388..bb719bc 100644 --- a/test/server/routes/index.test.js +++ b/test/server/routes/index.test.js @@ -4,10 +4,11 @@ const buildRoutes = require('server/routes') const buildMockRouter = require('../buildMockRouter') describe('routes', function () { - let router, route, res, req + let router, route, res, req, inputFile const testBaseFileName = 'bobby-tables' const testFileExtension = 'jpg' + const username = 'testuser' function buildUploadData ({ baseFileName=null, @@ -34,7 +35,9 @@ describe('routes', function () { } beforeEach(async function () { - req = {} + req = { + username, + } res = { send: (value) => { res.body = value }, } @@ -56,24 +59,18 @@ describe('routes', function () { describe('POST /api/files', function () { beforeEach(async function () { route = '/api/files' + inputFile = buildUploadData({}) }) it('uploads a file and returns its metadata', async function () { - const input = { - description: 'A portait of an artist', - file: { - name: 'bobby-tables.jpg', - mimetype: 'image/jpg', - base64: 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==', - }, - } - req.body = input + req.body = inputFile await router.postRoutes[route](req, res) - expect(res.body.id).to.be.ok - expect(res.body.description).to.equal(input.description) - expect(res.body.filename).to.equal(input.file.name) - expect(res.body.mimetype).to.equal(input.file.mimetype) - expect(res.body.src).to.equal(input.file.base64) + + validateTest({ + inputFile, + expectedFilename: inputFile.file.name, + responseBody: res.body, + }) }) }) }) From 452bbfbc53ac19ad77ba4ed903f88e93e8c29f67 Mon Sep 17 00:00:00 2001 From: ccornali611 Date: Thu, 14 Mar 2024 18:16:06 -0600 Subject: [PATCH 12/24] Added: test for getting different user files --- test/server/routes/index.test.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/test/server/routes/index.test.js b/test/server/routes/index.test.js index bb719bc..b8d92fe 100644 --- a/test/server/routes/index.test.js +++ b/test/server/routes/index.test.js @@ -2,13 +2,15 @@ const { expect } = require('chai') const db = require('db') const buildRoutes = require('server/routes') const buildMockRouter = require('../buildMockRouter') +const FileRepository = require('db/repositories/fileRepository') describe('routes', function () { - let router, route, res, req, inputFile + let router, route, res, req, inputFile, totalSeedTestFiles const testBaseFileName = 'bobby-tables' const testFileExtension = 'jpg' const username = 'testuser' + const fileRepository = new FileRepository(db.instance) function buildUploadData ({ baseFileName=null, @@ -48,11 +50,18 @@ describe('routes', function () { describe('GET /api/files', function () { beforeEach(async function () { route = '/api/files' + totalSeedTestFiles = fileRepository.getTotal() }) it('returns all the files', async function () { await router.getRoutes[route](req, res) - expect(res.body).to.have.length(5) + expect(res.body).to.have.length(totalSeedTestFiles) + }) + + it('returns 0 files for user with no uploads', async function () { + req.username = 'test1' + await router.getRoutes[route](req, res) + expect(res.body).to.have.length(0) }) }) From 2ce6c7feac661aa465e2bc10659638e78f2ddd7f Mon Sep 17 00:00:00 2001 From: ccornali611 Date: Thu, 14 Mar 2024 18:19:44 -0600 Subject: [PATCH 13/24] Added: tests for dup upload --- test/server/routes/index.test.js | 63 ++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/test/server/routes/index.test.js b/test/server/routes/index.test.js index b8d92fe..b451854 100644 --- a/test/server/routes/index.test.js +++ b/test/server/routes/index.test.js @@ -81,5 +81,68 @@ describe('routes', function () { responseBody: res.body, }) }) + + it('users uploads a file, same filename as file uploaded by different user', async function () { + const username2 = 'testuser2' + fileRepository.insertFile({ + description: inputFile.description, + filename: inputFile.file.name, + mimetype: inputFile.file.mimetype, + src: inputFile.file.base64, + username: username2, + }) + const totalRecords = fileRepository.getTotal() + expect(totalRecords).to.equal(totalSeedTestFiles + 1) + + const input = buildUploadData({}) + req.body = input + await router.postRoutes[route](req, res) + + validateTest({ + inputFile, + expectedFilename:input.file.name, + responseBody: res.body, + }) + }) + + it('uploads 1st duplicate, original exists in db', async function () { + fileRepository.insertFile({ + description: inputFile.description, + filename: inputFile.file.name, + mimetype: inputFile.file.mimetype, + src: inputFile.file.base64, + username, + }) + + // uploading a duplicate + req.body = inputFile + await router.postRoutes[route](req, res) + + validateTest({ + inputFile, + expectedFilename: `${testBaseFileName}(1).${testFileExtension}`, + responseBody: res.body, + }) + }) + + it('uploads 1st duplicate, original exists in db for user with 100+ duplicate files', async function () { + fileRepository.insertFile({ + description: inputFile.description, + filename: inputFile.file.name, + mimetype: inputFile.file.mimetype, + src: inputFile.file.base64, + username, + }) + + // uploading a duplicate + req.body = inputFile + await router.postRoutes[route](req, res) + + validateTest({ + inputFile, + expectedFilename: `${testBaseFileName}(1).${testFileExtension}`, + responseBody: res.body, + }) + }) }) }) From 16c225d6e72df0aa2f51e5188eeb8e35cae9e5df Mon Sep 17 00:00:00 2001 From: ccornali611 Date: Thu, 14 Mar 2024 18:22:05 -0600 Subject: [PATCH 14/24] Added: tests for dup upload to fill gaps --- test/server/routes/index.test.js | 60 +++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/test/server/routes/index.test.js b/test/server/routes/index.test.js index b451854..1d56925 100644 --- a/test/server/routes/index.test.js +++ b/test/server/routes/index.test.js @@ -44,7 +44,6 @@ describe('routes', function () { send: (value) => { res.body = value }, } router = buildRoutes(buildMockRouter()) - db.resetToSeed() }) describe('GET /api/files', function () { @@ -69,6 +68,7 @@ describe('routes', function () { beforeEach(async function () { route = '/api/files' inputFile = buildUploadData({}) + db.resetToSeed() }) it('uploads a file and returns its metadata', async function () { @@ -145,4 +145,62 @@ describe('routes', function () { }) }) }) + + describe('Upload duplicates fill gaps, original file named', function () { + + this.beforeAll(async function () { + route = '/api/files' + db.resetToSeed() + inputFile = buildUploadData({}) + fileRepository.bulkInsertFiles([ + { + description: inputFile.description, + filename: inputFile.file.name, + mimetype: inputFile.file.mimetype, + src: inputFile.file.base64, + username, + }, + { + description: inputFile.description, + filename: `${testBaseFileName}(1).${testFileExtension}`, + mimetype: inputFile.file.mimetype, + src: inputFile.file.base64, + username, + }, + { + description: inputFile.description, + filename: `${testBaseFileName}(3).${testFileExtension}`, + mimetype: inputFile.file.mimetype, + src: inputFile.file.base64, + username, + }, + { + description: inputFile.description, + filename: `${testBaseFileName}(5).${testFileExtension}`, + mimetype: inputFile.file.mimetype, + src: inputFile.file.base64, + username, + }, + ]) + }) + + const testCases = [ + `${testBaseFileName}(2).${testFileExtension}`, + `${testBaseFileName}(4).${testFileExtension}`, + `${testBaseFileName}(6).${testFileExtension}`, + ] + + testCases.forEach((testCase) => { + it(`uploaded file name should be "${testCase}"`, async function () { + req.body = inputFile + await router.postRoutes[route](req, res) + + validateTest({ + inputFile, + expectedFilename: testCase, + responseBody: res.body, + }) + }) + }) + }) }) From b82d9702d2c7e09647a9055efc075ebe2106f128 Mon Sep 17 00:00:00 2001 From: ccornali611 Date: Thu, 14 Mar 2024 18:25:46 -0600 Subject: [PATCH 15/24] Added: add'l dup tests --- test/server/routes/index.test.js | 76 ++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/test/server/routes/index.test.js b/test/server/routes/index.test.js index 1d56925..0c743ea 100644 --- a/test/server/routes/index.test.js +++ b/test/server/routes/index.test.js @@ -142,8 +142,84 @@ describe('routes', function () { inputFile, expectedFilename: `${testBaseFileName}(1).${testFileExtension}`, responseBody: res.body, + }) + }) + + describe('Uploading duplicate files, non-duplicate syntax file names', function () { + + it('uploads duplicate file, original does not exists in db', async function () { + // adding original file to db, but with the syntax of a duplicate filename + fileRepository.insertFile({ + description: inputFile.description, + filename: `${testBaseFileName}(1).${testFileExtension}`, + mimetype: inputFile.file.mimetype, + src: inputFile.file.base64, + username, + }) + + // uploading original file + req.body = inputFile + await router.postRoutes[route](req, res) + + validateTest({ + inputFile, + expectedFilename: inputFile.file.name, + responseBody: res.body, + }) + }) + + it('uploads 1st duplicate, original exists in db', async function () { + // adding original file to db + fileRepository.insertFile({ + description: inputFile.description, + filename: inputFile.file.name, + mimetype: inputFile.file.mimetype, + src: inputFile.file.base64, + username, + }) + + // uploading a duplicate + req.body = inputFile + await router.postRoutes[route](req, res) + + validateTest({ + inputFile, + expectedFilename: `${testBaseFileName}(1).${testFileExtension}`, + responseBody: res.body, + }) + }) + + it('uploads 2nd duplicate file, original exists in db', async function () { + // inserting previous uploads + fileRepository.bulkInsertFiles([ + { + description: inputFile.description, + filename: inputFile.file.name, + mimetype: inputFile.file.mimetype, + src: inputFile.file.base64, + username, + }, + { + description: inputFile.description, + filename: `${testBaseFileName}(1).${testFileExtension}`, + mimetype: inputFile.file.mimetype, + src: inputFile.file.base64, + username, + }, + ]) + + // uploading original file + req.body = inputFile + await router.postRoutes[route](req, res) + + validateTest({ + inputFile, + expectedFilename: `${testBaseFileName}(2).${testFileExtension}`, + responseBody: res.body, + }) }) }) + }) describe('Upload duplicates fill gaps, original file named', function () { From a6512faed329943f4dea2c367d9693595b4a73c0 Mon Sep 17 00:00:00 2001 From: ccornali611 Date: Thu, 14 Mar 2024 18:29:49 -0600 Subject: [PATCH 16/24] Added: test for filenaming edgecases --- test/server/routes/index.test.js | 156 +++++++++++++++++++++++++++---- 1 file changed, 136 insertions(+), 20 deletions(-) diff --git a/test/server/routes/index.test.js b/test/server/routes/index.test.js index 0c743ea..d6d96df 100644 --- a/test/server/routes/index.test.js +++ b/test/server/routes/index.test.js @@ -125,26 +125,6 @@ describe('routes', function () { }) }) - it('uploads 1st duplicate, original exists in db for user with 100+ duplicate files', async function () { - fileRepository.insertFile({ - description: inputFile.description, - filename: inputFile.file.name, - mimetype: inputFile.file.mimetype, - src: inputFile.file.base64, - username, - }) - - // uploading a duplicate - req.body = inputFile - await router.postRoutes[route](req, res) - - validateTest({ - inputFile, - expectedFilename: `${testBaseFileName}(1).${testFileExtension}`, - responseBody: res.body, - }) - }) - describe('Uploading duplicate files, non-duplicate syntax file names', function () { it('uploads duplicate file, original does not exists in db', async function () { @@ -220,6 +200,75 @@ describe('routes', function () { }) }) + describe('Uploading duplicate files, names in duplicate syntax', function () { + beforeEach(function () { + inputFile = buildUploadData({ baseFileName: 'test(1)', fileExt: 'png' }) + }) + + it('uploads orignal file', async function () { + req.body = inputFile + await router.postRoutes[route](req, res) + + + validateTest({ + inputFile, + expectedFilename: inputFile.file.name, + responseBody: res.body, + }) + }) + + it('uploads 1st duplicate file (2nd uploaded)', async function () { + // inserting original into DB + fileRepository.insertFile({ + description: inputFile.description, + filename: 'test(1).png', + mimetype: inputFile.file.mimetype, + src: inputFile.file.base64, + username, + }) + + // uploading duplicate of test(1).png + req.body = inputFile + await router.postRoutes[route](req, res) + + validateTest({ + inputFile, + expectedFilename: 'test(1)(1).png', + responseBody: res.body, + }) + }) + + it('uploads 2nd duplicate file (3rd uploaded)', async function () { + // inseting files into DB + fileRepository.bulkInsertFiles([ + { + description: inputFile.description, + filename: 'test(1).png', + mimetype: inputFile.file.mimetype, + src: inputFile.file.base64, + username, + }, + { + description: inputFile.description, + filename: 'test(1)(1).png', + mimetype: inputFile.file.mimetype, + src: inputFile.file.base64, + username, + }, + ]) + + // uploading 3rd file (2nd duplicate) of test(1).png + req.body = inputFile + await router.postRoutes[route](req, res) + + validateTest({ + inputFile, + expectedFilename: 'test(1)(2).png', + responseBody: res.body, + }) + }) + }) + }) describe('Upload duplicates fill gaps, original file named', function () { @@ -279,4 +328,71 @@ describe('routes', function () { }) }) }) + + describe('Upload duplicates fill gaps, original file name is already in duplicate syntax', function () { + this.beforeAll(async function () { + const baseTestFilename = `${testBaseFileName}(1)` + route = '/api/files' + inputFile = buildUploadData({ baseFileName: baseTestFilename }) + db.resetToSeed() + fileRepository.bulkInsertFiles([ + { + description: inputFile.description, + filename: inputFile.file.name, + mimetype: inputFile.file.mimetype, + src: inputFile.file.base64, + username, + }, + { + description: inputFile.description, + filename: `${baseTestFilename}(2).${testFileExtension}`, + mimetype: inputFile.file.mimetype, + src: inputFile.file.base64, + username, + }, + { + description: inputFile.description, + filename: `${baseTestFilename}(3).${testFileExtension}`, + mimetype: inputFile.file.mimetype, + src: inputFile.file.base64, + username, + }, + { + description: inputFile.description, + filename: `${baseTestFilename}(5).${testFileExtension}`, + mimetype: inputFile.file.mimetype, + src: inputFile.file.base64, + username, + }, + { + description: inputFile.description, + filename: `${baseTestFilename}(8).${testFileExtension}`, + mimetype: inputFile.file.mimetype, + src: inputFile.file.base64, + username, + }, + ]) + }) + + const testCases = [ + `${testBaseFileName}(1)(1).${testFileExtension}`, + `${testBaseFileName}(1)(4).${testFileExtension}`, + `${testBaseFileName}(1)(6).${testFileExtension}`, + `${testBaseFileName}(1)(7).${testFileExtension}`, + `${testBaseFileName}(1)(9).${testFileExtension}`, + ] + + testCases.forEach((testCase) => { + it(`uploaded file name should be "${testCase}"`, async function () { + req.body = inputFile + await router.postRoutes[route](req, res) + + validateTest({ + inputFile, + expectedFilename: testCase, + responseBody: res.body, + }) + }) + }) + }) }) From abcfcd3e5d7190d1329b77bd3e666fa4c72fdeb0 Mon Sep 17 00:00:00 2001 From: ccornali611 Date: Thu, 14 Mar 2024 21:36:02 -0600 Subject: [PATCH 17/24] Updated: fileRepository and server index, display paginated results --- src/db/repositories/fileRepository.js | 52 +++++++++++++++++++++------ 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/src/db/repositories/fileRepository.js b/src/db/repositories/fileRepository.js index 15f1fe7..ef98dec 100644 --- a/src/db/repositories/fileRepository.js +++ b/src/db/repositories/fileRepository.js @@ -19,9 +19,24 @@ class FileRepository { mimetype TEXT NOT NULL, src TEXT NOT NULL, username TEXT NOT NULL - ) + ); + `).run() + + this.db.prepare(` + CREATE INDEX IF NOT EXISTS idx_filename ON files (filename); + `).run() + + this.db.prepare(` + CREATE INDEX IF NOT EXISTS idx_filename ON files (filename); + `).run() + + this.db.prepare(` + CREATE INDEX IF NOT EXISTS idx_username ON files (username); + `).run() + + this.db.prepare(` + CREATE INDEX IF NOT EXISTS idx_src ON files (src); `).run() - // this.db.prepare('ADD COLUMN IF NOT EXISTS') } /** @@ -95,13 +110,19 @@ class FileRepository { /** * Retrieves paginated files associated with a specific user. * @param {string} username - The username of the user. + * @param {number} size - The maximum number of files to retrieve. + * @param {number} page - The number of files to skip before starting to return records. * @returns {object} An object of file records uploaded by the user and total. */ - getUserFiles ({ username }) { + getUserFiles ({ username, page, size }) { + page = page || 1 + size = size || 30 + const offset = (page -1) * size return this.db.prepare(` SELECT * FROM files WHERE username = :username - `).all({ username }) + LIMIT :limit OFFSET :offset + `).all({ username, limit: size, offset }) } /** @@ -116,11 +137,15 @@ class FileRepository { /** * Retrieves all files stored in the database. - * @returns {Array} An array of all file records stored. + * @param {number} size - The maximum number of files to retrieve. + * @param {number} page - The number of files to skip before starting to return records.s * @returns {Array} An array of all file records. */ - getAllFiles () { - return this.db.prepare(`SELECT * FROM files`).all() + getAllFiles ({ page=1, size=30 }) { + page = page || 1 + size = size || 30 + const offset = (page - 1) * size + return this.db.prepare(`SELECT * FROM files LIMIT :limit OFFSET :offset`).all({ limit: size, offset }) } /** @@ -143,7 +168,7 @@ class FileRepository { */ getOriginalFile ({ username, filename, base64 }) { return this.db.prepare(` - SELECT * + SELECT id, filename, src FROM files WHERE filename == :filename @@ -157,27 +182,32 @@ class FileRepository { }) } - // getCountMatchingCriteria /** * Retrieves all files similar to the provided criteria. Used to get the list of duplicate document * @param {Object} criteria - An object containing the criteria (username, filename, fileExt, and base64) for finding similar files. * @returns {Array} An object with records similar to the provided criteria and total matching in DB. */ - findAllLike ({ username, filename, fileExt, base64 }) { + findAllLike ({ username, filename, fileExt, base64, size=30, page=1 }) { + page = page || 1 + size = size || 30 + const offset = (page - 1) * size return this.db.prepare(` - SELECT * + SELECT id, filename, src FROM files WHERE filename == :filename OR filename LIKE :similarFilename AND src == :src AND username == :username ORDER BY filename ASC + LIMIT :limit OFFSET :offset `) .all({ filename: `${filename}.${fileExt}`, similarFilename: `${filename}(%.${fileExt}`, src: base64, username, + offset, + limit: size, }) } } From 31b618c07986a8ea977f3cf51451777ccc238f2a Mon Sep 17 00:00:00 2001 From: ccornali611 Date: Thu, 14 Mar 2024 21:36:59 -0600 Subject: [PATCH 18/24] Added: pagination tests --- test/server/routes/index.test.js | 117 +++++++++++++++++++++++++------ 1 file changed, 97 insertions(+), 20 deletions(-) diff --git a/test/server/routes/index.test.js b/test/server/routes/index.test.js index d6d96df..6ec9028 100644 --- a/test/server/routes/index.test.js +++ b/test/server/routes/index.test.js @@ -1,6 +1,7 @@ const { expect } = require('chai') const db = require('db') const buildRoutes = require('server/routes') +// const buildFileRoutes = require('server/routes/files') const buildMockRouter = require('../buildMockRouter') const FileRepository = require('db/repositories/fileRepository') @@ -9,7 +10,7 @@ describe('routes', function () { const testBaseFileName = 'bobby-tables' const testFileExtension = 'jpg' - const username = 'testuser' + const testUsername = 'testuser' const fileRepository = new FileRepository(db.instance) function buildUploadData ({ @@ -36,9 +37,36 @@ describe('routes', function () { expect(responseBody.src).to.equal(inputFile.file.base64) } + function generateTestFiles ({ baseFileName, count, missingNums=[], username }) { + const initFile = buildUploadData({ baseFileName }) + const files = [ + { + description: initFile.description, + filename: initFile.file.name, + mimetype: initFile.file.mimetype, + src: initFile.file.base64, + username: username || testUsername, + }, + ] + for (let i= 1; i < count; i++) { + if (!missingNums.includes(i)) { + const f = buildUploadData({ baseFileName: `${baseFileName}(${i})` }) + files.push({ + description: f.description, + filename: f.file.name, + mimetype: f.file.mimetype, + src: f.file.base64, + username: username || testUsername, + }) + } + } + return [initFile, files] + } + beforeEach(async function () { req = { - username, + username: testUsername, + query: {}, } res = { send: (value) => { res.body = value }, @@ -49,6 +77,7 @@ describe('routes', function () { describe('GET /api/files', function () { beforeEach(async function () { route = '/api/files' + db.resetToSeed() totalSeedTestFiles = fileRepository.getTotal() }) @@ -62,6 +91,33 @@ describe('routes', function () { await router.getRoutes[route](req, res) expect(res.body).to.have.length(0) }) + + it('display 1st page of user files', async function () { + req.username = 'test1' + const [, testFiles] = generateTestFiles({ + baseFileName: 'test1', + count: 50, + username: req.username, + }) + fileRepository.bulkInsertFiles(testFiles) + await router.getRoutes[route](req, res) + expect(res.body).to.have.length(30) + }) + + it('display 1st page of user files', async function () { + req.username = 'test1' + const [, testFiles] = generateTestFiles({ + baseFileName: 'test1', + count: 50, + username: req.username, + }) + req.query = { + page: 2, + } + fileRepository.bulkInsertFiles(testFiles) + await router.getRoutes[route](req, res) + expect(res.body).to.have.length(20) + }) }) describe('POST /api/files', function () { @@ -69,6 +125,7 @@ describe('routes', function () { route = '/api/files' inputFile = buildUploadData({}) db.resetToSeed() + totalSeedTestFiles = fileRepository.getTotal() }) it('uploads a file and returns its metadata', async function () { @@ -100,7 +157,7 @@ describe('routes', function () { validateTest({ inputFile, - expectedFilename:input.file.name, + expectedFilename: input.file.name, responseBody: res.body, }) }) @@ -111,7 +168,7 @@ describe('routes', function () { filename: inputFile.file.name, mimetype: inputFile.file.mimetype, src: inputFile.file.base64, - username, + username: testUsername, }) // uploading a duplicate @@ -134,7 +191,7 @@ describe('routes', function () { filename: `${testBaseFileName}(1).${testFileExtension}`, mimetype: inputFile.file.mimetype, src: inputFile.file.base64, - username, + username: testUsername, }) // uploading original file @@ -155,7 +212,7 @@ describe('routes', function () { filename: inputFile.file.name, mimetype: inputFile.file.mimetype, src: inputFile.file.base64, - username, + username: testUsername, }) // uploading a duplicate @@ -177,14 +234,14 @@ describe('routes', function () { filename: inputFile.file.name, mimetype: inputFile.file.mimetype, src: inputFile.file.base64, - username, + username: testUsername, }, { description: inputFile.description, filename: `${testBaseFileName}(1).${testFileExtension}`, mimetype: inputFile.file.mimetype, src: inputFile.file.base64, - username, + username: testUsername, }, ]) @@ -224,7 +281,7 @@ describe('routes', function () { filename: 'test(1).png', mimetype: inputFile.file.mimetype, src: inputFile.file.base64, - username, + username: testUsername, }) // uploading duplicate of test(1).png @@ -246,14 +303,14 @@ describe('routes', function () { filename: 'test(1).png', mimetype: inputFile.file.mimetype, src: inputFile.file.base64, - username, + username: testUsername, }, { description: inputFile.description, filename: 'test(1)(1).png', mimetype: inputFile.file.mimetype, src: inputFile.file.base64, - username, + username: testUsername, }, ]) @@ -269,6 +326,26 @@ describe('routes', function () { }) }) + // WIP + it.skip('uploads 1st duplicate, original exists in db for user with 100+ duplicate files', async function () { + const skipDupNums = [30, 49] + const [initFile, testFiles] = generateTestFiles({ + baseFileName: 'test1', + count: 100, + missingNums: skipDupNums, + }) + fileRepository.bulkInsertFiles(testFiles) + + // uploading a duplicate + req.body = initFile + await router.postRoutes[route](req, res) + + validateTest({ + inputFile: initFile, + expectedFilename: `test1(30).${testFileExtension}`, + responseBody: res.body, + }) + }) }) describe('Upload duplicates fill gaps, original file named', function () { @@ -283,28 +360,28 @@ describe('routes', function () { filename: inputFile.file.name, mimetype: inputFile.file.mimetype, src: inputFile.file.base64, - username, + username: testUsername, }, { description: inputFile.description, filename: `${testBaseFileName}(1).${testFileExtension}`, mimetype: inputFile.file.mimetype, src: inputFile.file.base64, - username, + username: testUsername, }, { description: inputFile.description, filename: `${testBaseFileName}(3).${testFileExtension}`, mimetype: inputFile.file.mimetype, src: inputFile.file.base64, - username, + username: testUsername, }, { description: inputFile.description, filename: `${testBaseFileName}(5).${testFileExtension}`, mimetype: inputFile.file.mimetype, src: inputFile.file.base64, - username, + username: testUsername, }, ]) }) @@ -341,35 +418,35 @@ describe('routes', function () { filename: inputFile.file.name, mimetype: inputFile.file.mimetype, src: inputFile.file.base64, - username, + username: testUsername, }, { description: inputFile.description, filename: `${baseTestFilename}(2).${testFileExtension}`, mimetype: inputFile.file.mimetype, src: inputFile.file.base64, - username, + username: testUsername, }, { description: inputFile.description, filename: `${baseTestFilename}(3).${testFileExtension}`, mimetype: inputFile.file.mimetype, src: inputFile.file.base64, - username, + username: testUsername, }, { description: inputFile.description, filename: `${baseTestFilename}(5).${testFileExtension}`, mimetype: inputFile.file.mimetype, src: inputFile.file.base64, - username, + username: testUsername, }, { description: inputFile.description, filename: `${baseTestFilename}(8).${testFileExtension}`, mimetype: inputFile.file.mimetype, src: inputFile.file.base64, - username, + username: testUsername, }, ]) }) From 2928b88db95406c9bf1c2ae55b6a3b3a9a2572ce Mon Sep 17 00:00:00 2001 From: ccornali611 Date: Thu, 14 Mar 2024 21:37:33 -0600 Subject: [PATCH 19/24] Updated: db test file --- test/server/db.test.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/server/db.test.js b/test/server/db.test.js index 6770550..c7580fa 100644 --- a/test/server/db.test.js +++ b/test/server/db.test.js @@ -12,15 +12,16 @@ describe('db', function () { res = db.instance .prepare(` INSERT INTO files - (description, filename, mimetype, src) + (description, filename, mimetype, src, username) VALUES - (@description, @filename, @mimetype, @src) + (@description, @filename, @mimetype, @src, @username) `) .run({ description: 'My file', filename, mimetype: 'text/plain', src: 'abc', + username: 'test', }) expect(res).to.be.ok }) From 4a0f9dd47db4bfd0994b1453c6d2673f49c6b3b6 Mon Sep 17 00:00:00 2001 From: ccornali611 Date: Thu, 14 Mar 2024 21:39:24 -0600 Subject: [PATCH 20/24] Updated: endpoint get files, display paginated results --- src/server/routes/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/routes/index.js b/src/server/routes/index.js index f5c4f33..ab8b5e1 100644 --- a/src/server/routes/index.js +++ b/src/server/routes/index.js @@ -14,7 +14,8 @@ function buildRoutes (router) { }) router.get('/api/files', async (req, res) => { - const files = fileRepository.getUserFiles({ username: req.username }) + const { page = 0, size = 30 } = req.query + const files = fileRepository.getUserFiles({ username: req.username, page: parseInt(page), size: parseInt(size) }) return res.send(files) }) @@ -45,7 +46,6 @@ function buildRoutes (router) { // If there are multiple duplicate db files, finding the next duplicate number (fill the gaps) let i = 0 while (i < existingFiles.length - 1) { - iters += 1 // if current file name identifier does not match the current iteration, breakout out of loop. Missing a duplicate, gap should be filled if (existingFiles[i].filename !== `${fileUploader.fileBaseName}(${i+1}).${fileUploader.fileExt}`) { break From 044eba660da14f3565a4e26f9d2b91b8d21a3d92 Mon Sep 17 00:00:00 2001 From: ccornali611 Date: Thu, 14 Mar 2024 21:41:43 -0600 Subject: [PATCH 21/24] Updated: seed.db file --- src/db/seed.db | Bin 36864 -> 36864 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/db/seed.db b/src/db/seed.db index 990039931309ba28d6ae3757ec4090e3422ae389..7c27ad7102a735c0905582295e0c215a03ee24db 100644 GIT binary patch delta 503 zcmZozz|^pSX+joTIRoEGzVeNQ?0l1N@&$1D z1f--ECl_TFlw{`TDTKI2geds=197O2j}8}135-t5%t_5l%uOYvE;lnbwWP8DkGgn> zk;O&Hgv=@}PA$T;6UAwoybKHstlXRo{LA>g_|EYqaC2@f{LaPOc+8xYlOa`JvAH{1 zcC(YT9kXa?zHgFYL{2DB14|AIJ43pnyu0k=9=TY-lGNf7kX=>G>1&T0i=4trC54Q^dV7`JU delta 365 zcmZozz|^pSX+jp;O$L5#zMGo`6*BoIm-7X2H3l)Vi;IghHZxDY%%?q>gI}jUCAB!Y zD6^m>Ge1uu#5E#BAuqo~A+IziM~6!R4AL@lQu7jXQ?bkDX6B}rR2JZnEiOt%)dn_G zlb3;kftA~gfqy+edko)8z7}q?jfE=QJdMZnSUDI{K~n+{<0d3v)NKG<@L)07`gbF8}}l From 0e9485310bb7b678d1bd56c65881fe4119bdace5 Mon Sep 17 00:00:00 2001 From: ccornali611 Date: Thu, 14 Mar 2024 21:50:57 -0600 Subject: [PATCH 22/24] Added: todo for fix --- src/server/routes/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/server/routes/index.js b/src/server/routes/index.js index ab8b5e1..30c27f7 100644 --- a/src/server/routes/index.js +++ b/src/server/routes/index.js @@ -44,6 +44,7 @@ function buildRoutes (router) { filename = fileUploader.generateDuplicateFilename(dbOriginalFile.filename) } else { // If there are multiple duplicate db files, finding the next duplicate number (fill the gaps) + // TODO: fix for 10+ duplicate files let i = 0 while (i < existingFiles.length - 1) { // if current file name identifier does not match the current iteration, breakout out of loop. Missing a duplicate, gap should be filled From 7f230cffe02d812ee3c948211aeda0746f95cec3 Mon Sep 17 00:00:00 2001 From: ccornali611 Date: Thu, 14 Mar 2024 21:53:05 -0600 Subject: [PATCH 23/24] Added: notes to README --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index dbb21df..cbd534e 100644 --- a/README.md +++ b/README.md @@ -52,3 +52,14 @@ You can add new files! Screen Shot 2020-05-07 at 12 30 03 PM That's all it does without your help! + + +#### Notes +I configured the application to only display files uploaded by the user +If there was functionality on the front end to look at files for a different user, that is possible +Some of changes I would make if this was truly for production: +1) Use pagination to display the files, have buttons for next, back and clicking on the individual pages +2) Allow user to sort by title and date uploaded (asc or desc) + - add updated_at and created_at columns to "files" table to facilitate +3) Have table for user and add `user_id` colmumn in "files" table +4) Implement authentication middleware and not get username from os From 98772ed6251446d95cd091c937ce7a8aafc585d0 Mon Sep 17 00:00:00 2001 From: ccornali611 Date: Fri, 15 Mar 2024 11:12:17 -0600 Subject: [PATCH 24/24] small fixes --- src/db/index.js | 3 +++ src/db/repositories/fileRepository.js | 13 ++++++++++--- src/server/routes/index.js | 2 +- test/server/routes/index.test.js | 1 - 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/db/index.js b/src/db/index.js index 0380a88..7cc2e65 100644 --- a/src/db/index.js +++ b/src/db/index.js @@ -1,6 +1,7 @@ const fs = require('fs') const path = require('path') const Database = require('better-sqlite3') +const FileRepository = require('db/repositories/fileRepository') const SEED_DB = path.join(__dirname, 'seed.db') const DEV_DB = path.join(__dirname, 'dev.db') @@ -27,5 +28,7 @@ module.exports = { resetToSeed () { copySeedDB({ force: true }) db = new Database(localDBPath) + const fileRepository = new FileRepository(db) + fileRepository.createTable() }, } diff --git a/src/db/repositories/fileRepository.js b/src/db/repositories/fileRepository.js index ef98dec..0497fcb 100644 --- a/src/db/repositories/fileRepository.js +++ b/src/db/repositories/fileRepository.js @@ -21,7 +21,15 @@ class FileRepository { username TEXT NOT NULL ); `).run() - + const schema = this.db.prepare("PRAGMA table_info('files')").all() + const usernameColumnExists = schema.some((column) => column.name === 'username') + + if (!usernameColumnExists) { + this.db.prepare(` + ALTER TABLE files ADD COLUMN username TEXT; + `).run() + } + // add column for duplicate count and orig_hashed_filename this.db.prepare(` CREATE INDEX IF NOT EXISTS idx_filename ON files (filename); `).run() @@ -53,7 +61,6 @@ class FileRepository { `) .run({ description, filename, mimetype, src, username }) const dbFile = this.getFileById(result.lastInsertRowid) - // console.log(`[INFO] FileRepository.insertFile(): "${filename}"`, dbFile) return dbFile } @@ -181,7 +188,7 @@ class FileRepository { src: base64, }) } - + /** * Retrieves all files similar to the provided criteria. Used to get the list of duplicate document * @param {Object} criteria - An object containing the criteria (username, filename, fileExt, and base64) for finding similar files. diff --git a/src/server/routes/index.js b/src/server/routes/index.js index 30c27f7..03c68c5 100644 --- a/src/server/routes/index.js +++ b/src/server/routes/index.js @@ -14,7 +14,7 @@ function buildRoutes (router) { }) router.get('/api/files', async (req, res) => { - const { page = 0, size = 30 } = req.query + const { page = 1, size = 30 } = req.query const files = fileRepository.getUserFiles({ username: req.username, page: parseInt(page), size: parseInt(size) }) return res.send(files) }) diff --git a/test/server/routes/index.test.js b/test/server/routes/index.test.js index 6ec9028..e94dbe6 100644 --- a/test/server/routes/index.test.js +++ b/test/server/routes/index.test.js @@ -1,7 +1,6 @@ const { expect } = require('chai') const db = require('db') const buildRoutes = require('server/routes') -// const buildFileRoutes = require('server/routes/files') const buildMockRouter = require('../buildMockRouter') const FileRepository = require('db/repositories/fileRepository')