In this assignment we will continue the project started in Assignment 1. Our goal is to be able to synchronized files on two different machines. For now, we will work on synchronizing files on two directories on the same machine. We will do it in a way that will allow for easy conversion to a two-machine solution.
In this assignment you will write two programs: a server and a client. The server will receive a local directory (a string) and a remote file list. The remote file list (RFL) will be in the format of a NULL-terminated array of strings. The server will create a second file list from the local directory and create three lists using the create_lists function from assignment 1. For each file in the RFL that is not in the local directory, the server will ask for a copy of the file from the client. For each file that is in both the RFL and the directory, the server will first ask the client for the modification time of the file and ask for the new file if it is newer than the one in the local directory.
The client and server will communicate using named pipes.
We will organize this assignment in such a way as to isolate the communication from the rest of the processing. That way when we start communicating between processes on different machines we can reuse much of the code from this assignment.
This assignment has several parts. Each of the parts is fairly simple if you have completed the other parts successfully.
Part 0: Getting Started
Put all of the utility functions you wrote for Assignment 1 in a file called
list_utility.c. Make a corresponding list_utility.h
include file. As you add functions to list_utility.c, update
list_utility.h. Get a copy of the restart library, restart.c
and its include file restart.h from the USP web site. Use these
functions in the following parts. This will make your program safe to use
in the presence of signals.
Add the following function to list_utility.c:
int compare_lists(char **list1, char **list2);
returns true (1) if list1 and list2 are identical
NULL-terminated arrays of strings, and false (0) otherwise. The lists
are identical if they have the same length and the strings they point
to have the same characters. Note that the lists contain pointers.
The pointers do not have to be identical, but the strings they point to
need to be equal. (Use strcmp.) Convince me that this function
works correctly.
Part 1: Directory functions
Write the following functions:
char **get_regular_files(char *dir)
returns a NULL-terminated array of strings containing the regular files
int the directory given by dir. Return NULL on error
with errno set. Use the stat function to determine if a file is a
regular file. You must allocate space for the array and the strings pointed
to by the elements of the array in such a way the the memory can be freed
with freemakeargv from USP. Look up this function in the program
index of USP. Use a method similar to Example 5.10 of USP to determine if
a file is a regular file. You will need to make two passes with
readdir, one to count the number of regular files (so you can
allocate space for the list) and one to put the entries in the list.
Note that you cannot just copy pointers from the DIR structure
into the list memory may be overwritten when you call readdir again.
int get_modification_time(char *filename)
returns an integer giving the number of seconds since the Epoch representing
the modification tome of the file. Return -1 with errno set if an error
occurs. Use the st_mtime field of the stat structure.
You can test this function by using ctime and comparing the result
generated by ls -l.
Convince me that these functions work correctly.
Part 2: Communication functions
In this part will will write a function to receive a file using a open
file descriptor. There are at least three ways for the receiver to
determine when
the end of the file was transmitted. One is to close the file descriptor
when the last byte has been sent. We do not want to do this since we need
to send more than one file. We can put a special terminator byte at the
end of the file, but this will not work for binary files.
Lastly we can tell the receiver how many bytes to expect. We will use this
method. The file size will be sent as
ASCII digits followed by a newline. No string terminator
will be sent. This will be immediately followed by the indicated number
of bytes.
Write the following functions and put them in list_utility.c.
int get_integer(int fd);
That reads a non-negative integer as described above from the open file
descriptor and returns its value. It returns -1 and sets error number
if an error occurs. Use the readline function from the restart
library. You can assume that a valid integer will not contain more than 64
digits, but you need to generate and error before a buffer overflow
occurs if the input is invalid.
int get_file(int fd, int size, char *filename);
Reads the given number of bytes from the open file descriptor and stores them
in the new file with the given name. If a file with the given name already
exists, the file is overwritten. Return the size of the file on success and
-1 on error with errno set. Decide on appropriate permissions for a new file.
int send_integer(int fd, int n);
that sends the second parameter (a non-negative integer)
over the open file descriptor in the format described above, ASCII digits
followed by a newline.
Return the value sent on success and -1 with errno set on error.
int send_n_bytes(int fd, int n, char *buf);
Sends exactly n bytes from the buffer to the given open file
descriptor. Return the number of bytes sent on success and -1 with errno set
on error.
int send_file(int fd, char *filename)
Sends the given file over the open file descriptor using the format described
above. First the size is sent as a single line and then the bytes of the
file are sent.
Return the size of the file on success and -1 with errno set on error.
This function should use the send_integer and send_n_bytes
functions above.
Test all of these functions. Write appropriate test programs to test them and generate output that demonstrates your success. It is your responsibility to convince me that these function are working properly.
Part 3:
File list utilities.
The server will need to receive a directory and a file
list from the client. We will send the directory as a line rather than as a
string. This means that it will be terminated by a newline rather than a
string terminator.
We do not want to send pointers when we send our list of strings over the
file descriptor. We will send each string as a line (as above) and send
an empty line to represent the end of the list.
Write the following functions and put them in
list_utility.
int send_string_as_line(int fd, char *buf);
takes an open file descriptor and a string as parameters and sends the
characters of the string (but not the string terminator)
to the file descriptor followed
by a newline character. Return the total number of bytes sent on success
or -1 with errno set on error. You may assume that the original string
does not contain a newline character.
int get_string_as_line(int fd, char *buf, int bufsize);
reads a line from fd and stores the result (without the newline)
in buf followed by a string terminator. The last parameter is the
size of the buffer and an error should be returned before a buffer overflow
occurs. Return the total number of bytes read on success or -1 with errno set
on error. To allow you to use the readline function of the restart
library, it is OK to return an error if the buffer is close to being too
small.
int send_string_list_as_lines(int fd, char **list);
The first parameter is an open file descriptor and the second parameter
is a NULL-terminated array of strings (like from Assignment 1). Each string
is converted to a line and sent to the file descriptor using
send_string_as_line. When the NULL pointer is reached an empty
line is sent (just a newline character). Return the total number of bytes
sent on success or -1 with errno set on error. We assume that none of the
original strings contains a newline characterer or is empty. This is a
reasonable assumption when the strings represent file names.
char **get_string_list_as_lines(int fd):
This reads lines in the format of send_string_list_as_lines and
converts them to a NULL-terminated array of strings using
get_string_as_line. Return the list on success and a NULL pointer
with errno set on failure. The returned list should consist of allocated
space that can be freed with freemakeargv from USP.
Implementing this is simpler if
you can assume a maximum length of the strings that are being sent and a
maximum number of strings to be sent. You
may assume a maximum string length of 1024 and a maximum number os strings
of 100. Use automatic storage for temporary space. Remember that you do not
want your returned list to be in automatic storage since this goes away
when the function returns.
You should not allow a buffer overflow to occur, no matter what input is
received, even if it contains more than 100 string or a string is longer than
1024.
Test these methods and convince me that they are working correctly. One way to test them is to create a list using makeargv, send the list to a pipe using send_string_list_as_lines and then reading it back with get_string_list_as_lines. Compare the result with the original. Write a method to do the comparison rather than just looking at the results. This will make sure no non-printing characters have crept into your list. Be sure to do some testing of send_string_as_line and get_string_as_line before writing the methods to transmit lists.
Part 4: Write the client.
The client takes 3 command-line parameters, the names
of two FIFOs, the first for reading and the second for writing and a directory.
The client should assume that these FIFOs already exist.
It sends the directory to the write pipe using send_string_as_line.
It creates a list of the regular files in the directory using
get_regular_files and sends this to the write pipe using
send_string_list_as_lines. The client then waits for requests from
the server be reading lines from the read pipe.
Three types of requests can be generated by the server. Each request is a line whose first character determines the type of request. The first character is an ASCII digit, '0', or '1', or '2'. For the first two requests, this is directly followed by the name of a file. For the last type, it is followed by a message.
Part 5: Write the server.
The server takes two command-line parameters, the names
of two FIFOs, the first for reading and the second for writing. The
server should assume that these FIFOs already exist. It will use
get_string_as_line to read a file name from the read FIFO
and get_string_list_as_lines to get a file list from the read FIFO.
The file name will be interpreted as a local directory and the
file list will be interpreted as a list of files on the remote directory.
Get a list of regular files on the local directory
(use get_regular_files)
and use create_lists to compare the local and remote directories.
For each file that is on the remote directory but not on the local one, request the file by sending the file name preceded by a '1' character. Get the file and save it in the local directory using get_file. For each file that is in both directories, get the modification time of the remote file and compare it to the modification time of the local one. If the remote one is newer, get it and overwrite the local one. Make sure this works even if the new version is smaller than the old one.
When all of this has been completed, send a message to the client preceded by a '2' character. The message should indicate the number of files successfully transferred if all were successful, or an appropriate error message if not.
Handing in your assignment
Use this cover sheet for Assignment 3 and
this one for Assignment 4.