diff --git a/2-StudentDB/sdbsc.c b/2-StudentDB/sdbsc.c new file mode 100644 index 0000000000000000000000000000000000000000..bc650c116bcaba990e660c101f77d5c2fddde490 --- /dev/null +++ b/2-StudentDB/sdbsc.c @@ -0,0 +1,587 @@ +#include <stdio.h> +#include <stdlib.h> +#include <fcntl.h> //c library for system call file routines +#include <string.h> +#include <sys/stat.h> +#include <unistd.h> +#include <stdbool.h> + +//database include files +#include "db.h" +#include "sdbsc.h" + +/* + * open_db + * dbFile: name of the database file + * should_truncate: indicates if opening the file also empties it + * + * returns: File descriptor on success, or ERR_DB_FILE on failure + * + * console: Does not produce any console I/O on success + * M_ERR_DB_OPEN on error + * + */ +int open_db(char *dbFile, bool should_truncate){ + // Set permissions: rw-rw---- + // see sys/stat.h for constants + mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP; + + //open the file if it exists for Read and Write, + //create it if it does not exist + int flags = O_RDWR | O_CREAT; + + if (should_truncate) + flags += O_TRUNC; + + // Now open file + int fd = open(dbFile, flags, mode); + + if (fd == -1) { + // Handle the error + printf(M_ERR_DB_OPEN); + return ERR_DB_FILE; + } + + return fd; +} + +/* + * get_student + * fd: linux file descriptor + * id: the student id we are looking for the name of + * *s: a pointer where the located (if found) student data will be + * copied + * + * returns: NO_ERROR student located and copied into *s + * ERR_DB_FILE database file I/O issue + * SRCH_NOT_FOUND student was not located in the database + * + * console: Does not produce any console I/O used by other functions + */ +int get_student(int fd, int id, student_t *s){ + if (fd == -1) return ERR_DB_FILE; + + int offset = id * STUDENT_RECORD_SIZE; + lseek(fd, offset, SEEK_SET); + + student_t student; + int bytes_read = read(fd, &student, STUDENT_RECORD_SIZE); + + if (bytes_read != STUDENT_RECORD_SIZE) { + return ERR_DB_FILE; + } + + if (student.id == DELETED_STUDENT_ID) { + return SRCH_NOT_FOUND; + } + + *s = student; + return NO_ERROR; +} + +/* + * add_student + * fd: linux file descriptor + * id: student id (range is defined in db.h ) + * fname: student first name + * lname: student last name + * gpa: GPA as an integer (range defined in db.h) + * + * Adds a new student to the database. After calculating the index for the + * student, check if there is another student already at that location. A good + * way is to use something like memcmp() to ensure that the location for this + * student contains all zero byes indicating the space is empty. + * + * returns: NO_ERROR student added to database + * ERR_DB_FILE database file I/O issue + * ERR_DB_OP database operation logically failed (aka student + * already exists) + * + * + * console: M_STD_ADDED on success + * M_ERR_DB_ADD_DUP student already exists + * M_ERR_DB_READ error reading or seeking the database file + * M_ERR_DB_WRITE error writing to db file (adding student) + * + */ +int add_student(int fd, int id, char *fname, char *lname, int gpa){ + if (fd == -1) return ERR_DB_FILE; + + int offset = id * STUDENT_RECORD_SIZE; + lseek(fd, offset, SEEK_SET); + + student_t existing_student; + int bytes_read = read(fd, &existing_student, STUDENT_RECORD_SIZE); + + if (bytes_read == STUDENT_RECORD_SIZE && existing_student.id != DELETED_STUDENT_ID) { + printf(M_ERR_DB_ADD_DUP, id); + return ERR_DB_OP; + } + + student_t new_student = {id, "", "", gpa}; + strncpy(new_student.fname, fname, sizeof(new_student.fname) - 1); + strncpy(new_student.lname, lname, sizeof(new_student.lname) - 1); + + lseek(fd, offset, SEEK_SET); + int bytes_written = write(fd, &new_student, STUDENT_RECORD_SIZE); + + if (bytes_written != STUDENT_RECORD_SIZE) { + printf(M_ERR_DB_WRITE); + return ERR_DB_FILE; + } + + printf(M_STD_ADDED, id); + return NO_ERROR; +} + +/* + * del_student + * fd: linux file descriptor + * id: student id to be deleted + * + * Removes a student to the database. Use the get_student() function to + * locate the student to be deleted. If there is a student at that location + * write an empty student record - see EMPTY_STUDENT_RECORD from db.h at + * that location. + * + * returns: NO_ERROR student deleted from database + * ERR_DB_FILE database file I/O issue + * ERR_DB_OP database operation logically failed (aka student + * not in database) + * + * + * console: M_STD_DEL_MSG on success + * M_STD_NOT_FND_MSG student not in database, cant be deleted + * M_ERR_DB_READ error reading or seeking the database file + * M_ERR_DB_WRITE error writing to db file (adding student) + * + */ +int del_student(int fd, int id){ + if (fd == -1) return ERR_DB_FILE; + + int offset = id * STUDENT_RECORD_SIZE; + lseek(fd, offset, SEEK_SET); + + student_t student; + int bytes_read = read(fd, &student, STUDENT_RECORD_SIZE); + + if (bytes_read != STUDENT_RECORD_SIZE || student.id == DELETED_STUDENT_ID) { + printf(M_STD_NOT_FND_MSG, id); + return ERR_DB_OP; + } + + student.id = DELETED_STUDENT_ID; + lseek(fd, offset, SEEK_SET); + write(fd, &student, STUDENT_RECORD_SIZE); + + printf(M_STD_DEL_MSG, id); + return NO_ERROR; +} + +/* + * count_db_records + * fd: linux file descriptor + * + * Counts the number of records in the database. Start by reading the + * database at the beginning, and continue reading individual records + * until you it EOF. EOF is when the read() syscall returns 0. Check + * if a slot is empty or previously deleted by investigating if all of + * the bytes in the record read are zeros - I would suggest using memory + * compare memcmp() for this. Create a counter variable and initialize it + * to zero, every time a non-zero record is read increment the counter. + * + * returns: <number> returns the number of records in db on success + * ERR_DB_FILE database file I/O issue + * ERR_DB_OP database operation logically failed (aka student + * not in database) + * + * + * console: M_DB_RECORD_CNT on success, to report the number of students in db + * M_DB_EMPTY on success if the record count in db is zero + * M_ERR_DB_READ error reading or seeking the database file + * M_ERR_DB_WRITE error writing to db file (adding student) + * + */ +int count_db_records(int fd){ + if (fd == -1) return ERR_DB_FILE; + + int count = 0; + student_t student; + lseek(fd, 0, SEEK_SET); + while (read(fd, &student, STUDENT_RECORD_SIZE) == STUDENT_RECORD_SIZE) { + if (student.id != DELETED_STUDENT_ID) { + count++; + } + } + + printf(M_DB_RECORD_CNT, count); + return count; +} + +/* + * print_db + * fd: linux file descriptor + * + * Prints all records in the database. Start by reading the + * database at the beginning, and continue reading individual records + * until you it EOF. EOF is when the read() syscall returns 0. Check + * if a slot is empty or previously deleted by investigating if all of + * the bytes in the record read are zeros - I would suggest using memory + * compare memcmp() for this. Be careful as the database might be empty. + * on the first real row encountered print the header for the required output: + * + * printf(STUDENT_PRINT_HDR_STRING, "ID", + * "FIRST NAME", "LAST_NAME", "GPA"); + * + * then for each valid record encountered print the required output: + * + * printf(STUDENT_PRINT_FMT_STRING, student.id, student.fname, + * student.lname, calculated_gpa_from_student); + * + * The code above assumes you are reading student records into a local + * variable named student that is of type student_t. Also dont forget that + * the GPA in the student structure is an int, to convert it into a real + * gpa divide by 100.0 and store in a float variable. + * + * returns: NO_ERROR on success + * ERR_DB_FILE database file I/O issue + * + * + * console: <see above> on success, print table or database empty + * M_ERR_DB_READ error reading or seeking the database file + * + */ +int print_db(int fd){ + if (fd == -1) return ERR_DB_FILE; + + student_t student; + lseek(fd, 0, SEEK_SET); + printf(STUDENT_PRINT_HDR_STRING, "ID", "FIRST NAME", "LAST NAME", "GPA"); + + while (read(fd, &student, STUDENT_RECORD_SIZE) == STUDENT_RECORD_SIZE) { + if (student.id != DELETED_STUDENT_ID) { + printf(STUDENT_PRINT_FMT_STRING, student.id, student.fname, student.lname, student.gpa/100.0); + } + } + + return NO_ERROR; +} + +/* + * print_student + * *s: a pointer to a student_t structure that should + * contain a valid student to be printed + * + * Start by ensuring that provided student pointer is valid. To do this + * make sure it is not NULL and that s->id is not zero. After ensuring + * that the student is valid, print it the exact way that is described + * in the print_db() function by first printing the header then the + * student data: + * + * printf(STUDENT_PRINT_HDR_STRING, "ID", + * "FIRST NAME", "LAST_NAME", "GPA"); + * + * printf(STUDENT_PRINT_FMT_STRING, s->id, s->fname, + * student.lname, calculated_gpa_from_s); + * + * Dont forget that the GPA in the student structure is an int, to convert + * it into a real gpa divide by 100.0 and store in a float variable. + * + * returns: nothing, this is a void function + * + * + * console: <see above> on success, print table or database empty + * M_ERR_STD_PRINT if the function argument s is NULL or if + * s->id is zero + * + */ +void print_student(student_t *s){ + if (s == NULL || s->id == DELETED_STUDENT_ID) { + printf(M_ERR_STD_PRINT); + return; + } + printf(STUDENT_PRINT_FMT_STRING, s->id, s->fname, s->lname, s->gpa / 100.0); +} + +/* + * NOTE IMPLEMENTING THIS FUNCTION IS EXTRA CREDIT + * + * compress_db + * fd: linux file descriptor + * + * This assignment takes advantage of the way Linux handles sparse files + * on disk. Thus if there is a large hole between student records, Linux + * will not use any physical storage. However, when a database record is + * deleted storage is used to write a blank - see EMPTY_STUDENT_RECORD from + * db.h - record. + * + * Since Linux provides no way to delete data in the middle of a file, and + * deleted records take up physical storage, this function will compress the + * database by rewriting a new database file that only includes valid student + * records. There are a number of ways to do this, but since this is extra credit + * you need to figure this out on your own. + * + * At a high level create a temporary database file then copy all valid students from + * the active database (passed in via fd) to the temporary file. When this is done + * rename the temporary database file to the name of the real database file. See + * the constants in db.h for required file names: + * + * #define DB_FILE "student.db" //name of database file + * #define TMP_DB_FILE ".tmp_student.db" //for extra credit + * + * Note that you are passed in the fd of the database file to be compressed, + * it is very likely you will need to close it to overwrite it with the + * compressed version of the file. To ensure the caller can work with the + * compressed file after you create it, it is a good design to return the fd + * of the new compressed file from this function + * + * returns: <number> returns the fd of the compressed database file + * ERR_DB_FILE database file I/O issue + * + * + * console: M_DB_COMPRESSED_OK on success, the db was successfully compressed. + * M_ERR_DB_OPEN error when opening/creating temporary database file. + * this error should also be returned after you + * compressed the database file and if you are unable + * to open it to pass the fd back to the caller + * M_ERR_DB_CREATE error creating the db file. For instance the + * inability to copy the temporary file back as + * the primary database file. + * M_ERR_DB_READ error reading or seeking the the db or tempdb file + * M_ERR_DB_WRITE error writing to db or tempdb file (adding student) + * + */ +int compress_db(int fd){ + printf(NOT_IMPLEMENTED_YET); + return fd; +} + + +/* + * validate_range + * id: proposed student id + * gpa: proposed gpa + * + * This function validates that the id and gpa are in the allowable ranges + * as per the specifications. It checks if the values are within the + * inclusive range using constents in db.h + * + * returns: NO_ERROR on success, both ID and GPA are in range + * EXIT_FAIL_ARGS if either ID or GPA is out of range + * + * console: This function does not produce any output + * + */ +int validate_range(int id, int gpa){ + + if ((id < MIN_STD_ID) || (id > MAX_STD_ID)) + return EXIT_FAIL_ARGS; + + if ((gpa < MIN_STD_GPA) || (gpa > MAX_STD_GPA)) + return EXIT_FAIL_ARGS; + + return NO_ERROR; +} + +/* + * usage + * exename: the name of the executable from argv[0] + * + * Prints this programs expected usage + * + * returns: nothing, this is a void function + * + * console: This function prints the usage information + * + */ +void usage(char *exename){ + printf("usage: %s -[h|a|c|d|f|p|z] options. Where:\n", exename); + printf("\t-h: prints help\n"); + printf("\t-a id first_name last_name gpa(as 3 digit int): adds a student\n"); + printf("\t-c: counts the records in the database\n"); + printf("\t-d id: deletes a student\n"); + printf("\t-f id: finds and prints a student in the database\n"); + printf("\t-p: prints all records in the student database\n"); + printf("\t-x: compress the database file [EXTRA CREDIT]\n"); + printf("\t-z: zero db file (remove all records)\n"); +} + + +//Welcome to main() +int main(int argc, char *argv[]){ + char opt; //user selected option + int fd; //file descriptor of database files + int rc; //return code from various operations + int exit_code; //exit code to shell + int id; //userid from argv[2] + int gpa; //gpa from argv[5] + + //space for a student structure which we will get back from + //some of the functions we will be writing such as get_student(), + //and print_student(). + student_t student = {0}; + + //This function must have at least one arg, and the arg must start + //with a dash + if ((argc < 2) || (*argv[1] != '-')){ + usage(argv[0]); + exit(1); + } + + //The option is the first character after the dash for example + //-h -a -c -d -f -p -x -z + opt = (char)*(argv[1]+1); //get the option flag + + //handle the help flag and then exit normally + if (opt == 'h'){ + usage(argv[0]); + exit(EXIT_OK); + } + + //now lets open the file and continue if there is no error + //note we are not truncating the file using the second + //parameter + fd = open_db(DB_FILE, false); + if (fd < 0){ + exit(EXIT_FAIL_DB); + } + + //set rc to the return code of the operation to ensure the program + //use that to determine the proper exit_code. Look at the header + //sdbsc.h for expected values. + + exit_code = EXIT_OK; + switch(opt){ + case 'a': + // arv[0] arv[1] arv[2] arv[3] arv[4] arv[5] + //prog_name -a id first_name last_name gpa + //------------------------------------------------------- + //example: prog_name -a 1 John Doe 341 + if (argc != 6){ + usage(argv[0]); + exit_code = EXIT_FAIL_ARGS; + break; + } + + //convert id and gpa to ints from argv. For this assignment assume + //they are valid numbers + id = atoi(argv[2]); + gpa = atoi(argv[5]); + + exit_code = validate_range(id,gpa); + if (exit_code == EXIT_FAIL_ARGS){ + printf(M_ERR_STD_RNG); + break; + } + + rc = add_student(fd, id, argv[3], argv[4], gpa); + if (rc < 0) + exit_code = EXIT_FAIL_DB; + + break; + + case 'c': + // arv[0] arv[1] + //prog_name -c + //----------------- + //example: prog_name -c + rc = count_db_records(fd); + if (rc < 0) + exit_code = EXIT_FAIL_DB; + break; + + case 'd': + // arv[0] arv[1] arv[2] + //prog_name -d id + //------------------------- + //example: prog_name -d 100 + if (argc != 3){ + usage(argv[0]); + exit_code = EXIT_FAIL_ARGS; + break; + } + id = atoi(argv[2]); + rc = del_student(fd, id); + if (rc < 0) + exit_code = EXIT_FAIL_DB; + + break; + + case 'f': + // arv[0] arv[1] arv[2] + //prog_name -f id + //------------------------- + //example: prog_name -f 100 + if (argc != 3){ + usage(argv[0]); + exit_code = EXIT_FAIL_ARGS; + break; + } + id = atoi(argv[2]); + rc = get_student(fd, id, &student); + + + switch (rc){ + case NO_ERROR: + print_student(&student); + break; + case SRCH_NOT_FOUND: + printf(M_STD_NOT_FND_MSG, id); + exit_code = EXIT_FAIL_DB; + break; + default: + printf(M_ERR_DB_READ); + exit_code = EXIT_FAIL_DB; + break; + } + break; + + case 'p': + // arv[0] arv[1] + //prog_name -p + //----------------- + //example: prog_name -p + rc = print_db(fd); + if (rc < 0) + exit_code = EXIT_FAIL_DB; + break; + + case 'x': + // arv[0] arv[1] + //prog_name -x + //----------------- + //example: prog_name -x + + //remember compress_db returns a fd of the compressed database. + //we close it after this switch statement + fd = compress_db(fd); + if (fd < 0) + exit_code = EXIT_FAIL_DB; + break; + + case 'z': + // arv[0] arv[1] + //prog_name -x + //----------------- + //example: prog_name -x + //HINT: close the db file, we already have fd + // and reopen db indicating truncate=true + close(fd); + fd = open_db(DB_FILE, true); + if (fd < 0){ + exit_code = EXIT_FAIL_DB; + break; + } + printf(M_DB_ZERO_OK); + exit_code = EXIT_OK; + break; + default: + usage(argv[0]); + exit_code = EXIT_FAIL_ARGS; + } + + //dont forget to close the file before exiting, and setting the + //proper exit code - see the header file for expected values + close(fd); + exit(exit_code); +}