/* The software in this package is distributed under the GNU General Public License version 2 (with a special exception described below). A copy of GNU General Public License (GPL) is included in this distribution, in the file COPYING.GPL. As a special exception, if other files instantiate templates or use macros or inline functions from this file, or you compile this file and link it with other works to produce a work based on this file, this file does not by itself cause the resulting work to be covered by the GNU General Public License. However the source code for this file must still be made available in accordance with section (3) of the GNU General Public License. This exception does not invalidate any other reasons why a work based on this file might be covered by the GNU General Public License. */ /*************************************************************************** filefunc.cpp - functions for working with FS ------------------- begin : Sun Nov 14 1999 copyright : (C) 1999 by Intra2net AG ***************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "filefunc.hxx" #include "stringfunc.hxx" namespace I2n { using namespace std; /* ** implementation of Stat */ Stat::Stat() : FollowLinks(true) , Valid(false) { clear(); } // eo Stat::Stat() Stat::Stat(const std::string& path, bool follow_links) { stat(path,follow_links); } // eo Stat::Stat(const std::string&,bool) Stat::~Stat() { } // eo Stat::~Stat() /** * @brief updates the internal data. * * In other words: stat()'s the file again. */ void Stat::recheck() { if (! Path.empty()) { // pass a copy of Path: otherwise clear() would leave an empty reference stat(string(Path), FollowLinks); } } // eo Stat::recheck() /** * @brief calls stat() or lstat() to get the information for the given path * and stores that information. * @param path the path which should be checked * @param follow_links determine if (symbalic) links should be followed. */ void Stat::stat(const std::string& path, bool follow_links) { clear(); Path= path; FollowLinks= follow_links; struct stat stat_info; int res; res = ( follow_links ? ::stat(path.c_str(), &stat_info) : ::lstat(path.c_str(), &stat_info) ); if ( 0 == res ) { Device = stat_info.st_dev; Inode = stat_info.st_ino; Mode = (stat_info.st_mode & ~(S_IFMT)); NumLinks = stat_info.st_nlink; Uid = stat_info.st_uid; Gid = stat_info.st_gid; DeviceType = stat_info.st_rdev; Size = stat_info.st_size; Atime = stat_info.st_atime; Mtime = stat_info.st_mtime; Ctime = stat_info.st_atime; // the stat(2) manpage for linux defines that st_blocks is given in a number of 512-byte-blocks. BytesOnDisk = stat_info.st_blocks; BytesOnDisk*=(long long)512; IsLink= S_ISLNK( stat_info.st_mode ); IsRegular= S_ISREG( stat_info.st_mode ); IsDirectory= S_ISDIR( stat_info.st_mode ); IsCharacterDevice= S_ISCHR( stat_info.st_mode ); IsBlockDevice= S_ISBLK( stat_info.st_mode ); IsFifo= S_ISFIFO( stat_info.st_mode ); IsSocket= S_ISSOCK( stat_info.st_mode ); } Valid = (0 == res); } // eo Stat::stat(const std::string&,bool) /** * @brief clears the internal data. */ void Stat::clear() { Path.clear(); Valid= false; Device = 0; Inode = 0; Mode = 0; NumLinks = 0; Uid = 0; Gid = 0; DeviceType = 0; Size = 0; BytesOnDisk = 0; Atime = 0; Mtime = 0; Ctime = 0; IsLink= false; IsRegular= false; IsDirectory= false; IsCharacterDevice= false; IsBlockDevice= false; IsFifo= false; IsSocket= false; } // eo Stat::clear() /** * @brief checks if another instance describes the same file. * @param rhs the other instance. * @return @a true iff the other instance describes the same file. * @note * The "same file" means that the files are located on the same device and use the same inode. * They might still have two different directory entries (different paths)! */ bool Stat::is_same_as(const Stat& rhs) { return Valid and rhs.Valid and ( Device == rhs.Device) and ( Inode == rhs.Inode); } // eo Stat::is_same_as(const Stat& rhs); /** * @brief checks if this and the other instance describe the same device. * @param rhs the other instance. * @return @a true if we and the other instance describe a device and the same device. * * "Same device" means that the devices have the same type and the same major and minor id. */ bool Stat::is_same_device_as(const Stat& rhs) { return is_device() and rhs.is_device() and ( IsBlockDevice == rhs.IsBlockDevice ) and ( IsCharacterDevice == rhs.IsCharacterDevice ) and ( DeviceType == rhs.DeviceType); } // eo Stat::is_same_device_as(const Stat&) /** * @brief check existence of a path. * @param path path which should be tested. * @return @a true iff path exists. */ bool path_exists(const std::string& path) { struct stat stat_info; int res = ::stat(path.c_str(), &stat_info); if (res) return false; return true; } // eo path_exists(const std::string&) /** * @brief check existence of a regular file. * @param path path which should be tested. * @return @a true if path exists and is a regular file (or a link pointing to a regular file). * @note this checks for regular files; not for the pure exitsnace of a path; use pathExists() for that. * @see pathExists */ bool file_exists(const std::string& path) { struct stat stat_info; int res = ::stat(path.c_str(), &stat_info); if (res) return false; return S_ISREG(stat_info.st_mode); } // eo file_exists(const std::string&) // TODO: Use Stat class /** * Get size of file * @param name filename to get size for * @return file size or -1 if file does not exist */ long file_size (const string &name) { long iReturn = -1; struct stat statbuff; if (lstat(name.c_str(), &statbuff) < 0) return -1; if (!S_ISREG(statbuff.st_mode)) return -1; iReturn=statbuff.st_size; return iReturn; } /** * @brief tests the last modification time stamp of a path. * @param path path which should be tested. * @return the last modification time or 0 if the path doen't exist. */ time_t file_mtime(const std::string& path) { struct stat stat_info; int res = ::stat(path.c_str(), &stat_info); if (res) return 0; return stat_info.st_mtime; } // eo file_mtime(const std::string&) /** * @brief Check if two files differ * * Note: Reads the whole file into memory * if the file size is identical. * * @param old_filename Filename of old file * @param new_filename Filename of new file * * @return bool True if files differ, false otherwise. * If one file does not exist, also returns true */ bool file_content_differs(const std::string &old_filename, const std::string &new_filename) { if (I2n::file_exists(old_filename) == false || I2n::file_exists(new_filename) == false) return true; // check if size differs if (I2n::file_size(old_filename) != I2n::file_size(new_filename)) return true; const std::string old_content = I2n::read_file(old_filename); const std::string new_content = I2n::read_file(new_filename); // check if content differs if (old_content == new_content) return false; // Differ by default (fallback) return true; } /** * @brief reads the contents of a directory. * @param path the path to the directory whose contents should be read. * @param[out] result the resulting list of names. * @param include_dot_names determines if dot-files should be included in the list. * @return @a true if reading the directory was succesful, @a false on error. */ bool get_dir( const std::string& path, std::vector< std::string >& result, bool include_dot_names ) { // code copied to get_dir_count; keep in sync DIR* dir = ::opendir( path.c_str()); if (!dir) { return false; } struct dirent store, *entry = NULL; while (readdir_r(dir, &store, &entry) == 0 && entry != NULL) { std::string name( entry->d_name ); if (! include_dot_names && (name[0] == '.') ) { continue; } result.push_back( name ); } ::closedir(dir); return true; } // eo get_dir(const std::string&,std::vector< std::string >&,bool) /** * @brief reads the contents of a directory * @param path the path to the directory whose contents should be read. * @param include_dot_names determines if dot-files should be included in the list. * @return the list of names (empty on error). */ std::vector< std::string > get_dir(const std::string& path, bool include_dot_names ) { std::vector< std::string > result; get_dir(path,result,include_dot_names); return result; } // eo get_dir(const std::string&,bool) /** * @brief count entries in directory, like get_dir(path, include_dot_names).size() * @param path the path to the directory whose contents should be read. * @param include_dot_names determines if dot-files should be included in the count. * @return the number of entries in the directory; return -1 in case of error */ int get_dir_count(const std::string& path, bool include_dot_names) { // code is a simplified copy of get_dir, above. Keep in sync int result = 0; DIR* dir = ::opendir( path.c_str()); if (!dir) return -1; struct dirent store, *entry = NULL; while (readdir_r(dir, &store, &entry) == 0 && entry != NULL) { if (entry->d_name == NULL) continue; // should not happen else if (! include_dot_names && (entry->d_name)[0] == '.') continue; ++result; } ::closedir(dir); return result; } /** * @brief removes a file from a filesystem. * @param path path to the file. * @return @a true if the unlink was successful. */ bool unlink( const std::string& path ) { int res = ::unlink( path.c_str() ); return (res == 0); } // eo unlink(const std::string&) /** * @brief creates a symbolic link named @a link_name to @a target. * @param target the target the link should point to. * @param link_name the name of the link. * @param force if @a true, the (file or link) @a link_name is removed if it exists. * @return @a true iff the symlink was successfully created. */ bool symlink(const std::string& target, const std::string& link_name, bool force) { int res= -1; if (target.empty() or link_name.empty() or target == link_name) { // no, we don't do this! return false; } std::string n_target; if (target[0] == '/') // absolute target? { n_target= target; } else // relative target { // for stat'ing: prepend dir of link_name: n_target= dirname(link_name)+"/"+ target; } Stat target_stat(n_target, false); Stat link_name_stat(link_name, false); if (target_stat.exists() && link_name_stat.exists()) { if (link_name_stat.is_same_as(target_stat) or link_name_stat.is_same_device_as(target_stat) ) { return false; } //TODO: more consistency checks?! } if (link_name_stat.exists()) { // the link name already exists. if (force) { // "force" as given, so try to remove the link_name: unlink(link_name); // update the stat: link_name_stat.recheck(); } } if (link_name_stat.exists()) { // well, if the link_name still exists; we cannot create that link: errno = EEXIST; return false; } res= ::symlink(target.c_str(), link_name.c_str()); return (0 == res); } // eo symlink(const std::string&,const std::string&,bool) /** * @brief reads the target of a symbolic link. * @param path path to the symbolic link * @return the target of the link or an empty string on error. */ std::string read_link(const std::string& path) { errno= 0; Stat stat(path,false); if (!stat || !stat.is_link()) { return std::string(); } int buffer_size= PATH_MAX+1 + 128; boost::scoped_array buffer_ptr( new char[buffer_size] ); int res= ::readlink( path.c_str(), buffer_ptr.get(), buffer_size-1 ); if (res >= 0) { return std::string( buffer_ptr.get(), res ); } return std::string(); } // eo read_link(const std::string&) /** * @brief returns content of a file as string. * * A simple (q'n'd) function for retrieving content of a file as string.
* Also able to read special (but regular) files which don't provide a size when stat'ed * (like files in the /proc filesystem). * * @param path path to the file. * @return the content of the file as string (empty if file could be opened). */ std::string read_file(const std::string& path) { Stat stat(path); if (!stat.is_reg()) { return std::string(); } std::ifstream f( path.c_str(), std::ios::in | std::ios::binary ); std::string result; if (f.is_open()) { // NOTE: there are cases where we don't know the correct size (/proc files...) // therefore we only use the size for reserving space if we know it, but don't // use it when reading the file! if (stat.size() > 0) { // if we know the size, we reserve enough space. result.reserve( stat.size() ); } char buffer[2048]; while (f.good()) { f.read(buffer, sizeof(buffer)); result.append(buffer, f.gcount()); } } return result; } // eo read_file(const std::string&) /** * @brief writes a string to a file. * @param path path to the file * @param data the data which should be written into the file * @param trunc set the trunc flag when opening the file. Do not use for files in /proc and /sys * @return @a true if the data was written to the file. * * A simple (q'n'd) function for writing a string to a file. */ bool write_file(const std::string& path, const std::string& data, bool trunc) { // set the correct openmode flags std::ios_base::openmode flags = std::ios::out | std::ios::binary; if (trunc) flags |= std::ios::trunc; std::ofstream f( path.c_str(), flags); if (f.good()) { f.write( data.data(), data.size() ); return f.good(); } else { return false; } } // eo write_file(const std::string&,const std::string&) /** * Copy file in 4k blocks from source to target. * Overwrites the target if it already exists. * * On error the target file gets removed. * * @param src source file * @param dest target file * @return true if all is ok, false on error */ bool copy_file(const std::string& src, const std::string& dest) { std::ifstream input( src.c_str(), std::ios::in | std::ios::binary ); if (!input) return false; std::ofstream output( dest.c_str(), std::ios::out | std::ios::binary | std::ios::trunc); if (!output) return false; // Out of disc space? if (!copy_stream(input,output)) { output.close(); unlink(dest); return false; } return true; } /** * Copy streams in 4k blocks. * * @param is source stream * @param os target stream * @return true if all is ok, false on error */ bool copy_stream(std::istream& is, std::ostream& os) { if (!is) return false; if (!os) return false; char buffer[4096]; while (is.good()) { is.read(buffer, sizeof(buffer)); os.write(buffer, is.gcount()); // Can't write? if (!os.good()) return false; } return true; } /** * @brief returns the filename part of a path (last component) * @param path the path. * @return the last component of the path. */ std::string basename(const std::string& path) { std::string::size_type pos= path.rfind('/'); if (pos != std::string::npos) { return path.substr(pos+1); } return path; } // eo basename(const std::string&) /** * @brief returns the directory part of a path. * @param path the path. * @return the directory part of the path. */ std::string dirname(const std::string& path) { std::string::size_type pos= path.rfind('/'); if (pos != std::string::npos) { std::string result(path,0,pos); if (result.empty()) { return "."; } return result; } return "."; } // eo dirname(const std::string&) /** * @brief normalizes a path. * * This method removes empty and "." elements. * It also resolves ".." parts by removing previous path elements if possible. * Leading ".." elements are preserved when a relative path was given; else they are removed. * Trailing slashes are removed. * * @param path the path which should be normalized. * @return the normalized path. */ std::string normalize_path(const std::string& path) { if (path.empty()) { return std::string(); } // remember if the given path was absolute since this information vanishes when // we split the path (since we split with omitting empty parts...) bool is_absolute= (path[0]=='/'); std::list< std::string > parts; std::list< std::string > result_parts; split_string(path,parts,"/",true); for(std::list< std::string >::const_iterator it_parts= parts.begin(); it_parts != parts.end(); ++it_parts) { std::string part(*it_parts); //convenience.. if (part == std::string(".") ) { // single dot is "current path"; ignore! continue; } if (part == std::string("..") ) { // double dot is "one part back" if (result_parts.empty()) { if (is_absolute) { // ignore since we cannot move behind / on absolute paths... } else { // on relative path, we need to store the "..": result_parts.push_back(part); } } else if (result_parts.back() == std::string("..") ) { // if last element was already "..", we need to store the new one again... // (PS: no need for "absolute" check; this can only be the case on relative path) result_parts.push_back(part); } else { // remove last element. result_parts.pop_back(); } continue; } result_parts.push_back(part); } std::string result; if (is_absolute) { result= "/"; } result+= join_string(result_parts,"/"); return result; } // eo normalize_path(const std::string&) /** * @brief calls fsync on a given directory to sync all it's metadata * @param path the path of the directory. * @return true if successful */ bool dirsync(const std::string& path) { // sync the directory the file is in DIR* dir=opendir(path.c_str()); if (dir == NULL) return false; int ret=fsync(dirfd(dir)); closedir(dir); return (ret==0); } /** * @brief changes the file(/path) mode. * @param path the path to change the mode for. * @param mode the new file mode. * @return @a true iff the file mode was sucessfully changed. */ bool chmod(const std::string& path, int mode) { int res= ::chmod(path.c_str(), mode); return (res==0); } // eo chmod(const std::string&,int) /** * @brief changed the owner of a file(/path) * @param path the path to change the owner for. * @param user the new file owner. * @param group the new file group. * @return @a true iff the file owner was succesfully changed. * * @note * the validity of user and group within the system is not checked. * This is intentional since this way we can use id's which are not assigned. */ bool chown(const std::string& path, const I2n::User& user, const I2n::Group& group) { uid_t uid= user.Uid; if (uid<0) return false; gid_t gid= group.Gid; if (gid<0) gid= user.Gid; if (gid<0) return false; int res= ::chown( path.c_str(), uid, gid); return (res==0); } // eo chown(const std::string&,const User&,const Group&) /** * Recursive delete of files and directories * @param path File or directory to delete * @param keep_parent_dir Keep parent directory (=empty out directory) [optional] * @param error Will contain the error if the return value is false [optional] * @return true on success, false otherwise */ bool recursive_delete(const std::string &path, bool keep_parent_dir, std::string *error) { bool rtn = true; try { Stat sp(path, false); if (!sp) throw runtime_error("can't stat " + path); if (sp.is_directory()) { std::vector dirents = get_dir(path, false); BOOST_FOREACH(const std::string &filename, dirents) { // Delete subdir or file. rtn = recursive_delete(path + "/" + filename, false, error); if (rtn == false) break; } if (keep_parent_dir == false && !rmdir(path)) throw runtime_error("can't remove directory " + path); } else { if (!unlink(path)) throw runtime_error("can't unlink " + path); } } catch (exception &e) { if (error) { ostringstream out; out << e.what() << " (" << strerror(errno) << ")"; *error = out.str(); } rtn = false; } catch (...) { if (error) { ostringstream out; out << "unknown error (" << strerror(errno) << ")"; *error = out.str(); } rtn = false; } return rtn; } // eo recursive_delete(const std::string&,std::string*) /** Create a unique temporary directory from path_template. @param Path template. The last six characters must be XXXXXX. @param error Will contain the error if the return value is empty [optional] @return Name of new directory or empty string on error. @seealso: classes in tmpfstream which offer functionality based on mkstemp */ std::string mkdtemp(const std::string &path_template, std::string *error) { boost::scoped_array buf( new char[path_template.size()+1] ); path_template.copy(buf.get(), path_template.size()); buf[path_template.size()]=0; char *unique_dir = ::mkdtemp(buf.get()); if (!unique_dir) { if (error) *error = strerror(errno); return ""; } // Scoped pointer is still valid return std::string(unique_dir); } /** Create directory @param path Path to create @param error Will contain the error if the return value is false [optional] @return True on success, false on error */ bool mkdir(const std::string &path, const mode_t &mode, std::string *error) { if ( ::mkdir(path.c_str(), mode) == 0) return true; if (error) *error = strerror(errno); return false; } /** Remove directory @param path Path to removed @param error Will contain the error if the return value is false [optional] @return True on successs, false otherwise */ bool rmdir(const std::string &path, std::string *error) { if ( ::rmdir(path.c_str() ) == 0) return true; if (error) *error = strerror(errno); return false; } /// Small helper class for scoped free class scoped_C_free { public: scoped_C_free(void *ptr) : pointer_to_free(ptr) { } ~scoped_C_free() { free (pointer_to_free); pointer_to_free = NULL; } private: void *pointer_to_free; }; /** Get current working directory @return Current working directory. Empty string on error. */ std::string getcwd() { char *cwd = ::getcwd(NULL, 0); if (!cwd) return ""; // Make deallocation of cwd exception safe scoped_C_free holder(cwd); string current_dir(cwd); return current_dir; } /** Change current working directory @param path Path to change to @param error Will contain the error if the return value is false [optional] @return True on successs, false otherwise */ bool chdir(const std::string &path, std::string *error) { if ( ::chdir(path.c_str() ) == 0) return true; if (error) *error = strerror(errno); return false; } /** Set file mode creation mask @param mask Creation mask @return Previous creation mask (function call always succeeds) */ mode_t umask(mode_t mask) { return ::umask(mask); } /** * @brief Remove unlisted files * * @param directory Directory to look for files * @param keep_files List of files or directories to keep * @param prefix Filename prefix to match. Empty prefix matches all. * * @return bool True if the directory was scanned, false on error (directory not found, permission denied) **/ bool remove_unlisted_files(const std::string &directory, const std::set &keep_files, const std::string &prefix) { std::vector content; if (!get_dir(directory, content, false)) return false; bool all_fine = true; BOOST_FOREACH(const std::string &file, content) { // Check for filename prefix (if any) if (!prefix.empty() && file.find(prefix) != 0) continue; // Check if file is whitelisted if (keep_files.find(file) != keep_files.end()) continue; // Try to unlink file. (Continue on error) if (!unlink(directory + "/" + file)) all_fine = false; } return all_fine; } /** * @brief Get free size in bytes on a given path or filename * * @param path Directory or filename to look in * * @return Number of bytes available to a regular user, -1 in case of an error **/ long long get_free_diskspace(const std::string& path) { struct statvfs sf; int looplimit=10000; int ret; while ( ((ret=statvfs(path.c_str(),&sf)) == -1) && (errno==EINTR) && looplimit > 0) looplimit--; if (ret==-1) { // a real error occured return -1; } long long free_bytes=0; // load block size free_bytes=sf.f_bsize; // multiply by number of free blocks accessible by normal users // make sure we really multiply long long by long long and don't overflow at 2 GB free_bytes*=(long long)sf.f_bavail; return free_bytes; } namespace { // anonymous namespace to make du_internal inaccessible from outside // internally used by du, do not use for other things void du_internal(const std::string &path, long long &sum, std::map > &counted_inodes) { Stat sp(path, false); // don't dereference symlinks here if (!sp) throw runtime_error("can't stat " + path); // make sure we don't count hardlinked files twice bool count_file=true; // dirs can't be hardlinked, their nlink is the size of entries -> doesn't matter for us here if (!sp.is_directory() && sp.nlink() > 1) { // see if we have remembered this dev / inode combination if (counted_inodes[sp.device()].count(sp.inode())) count_file=false; else counted_inodes[sp.device()].insert(sp.inode()); } // always add the space used, even if we have a directory, symlink or whatever: // they need space on disk too if (count_file) sum+=sp.bytes_on_disk(); if (sp.is_directory()) { std::vector dirents = get_dir(path, false); BOOST_FOREACH(const std::string &filename, dirents) { // calculate size of subdir or file du_internal(path + "/" + filename, sum, counted_inodes); } } } } // eo anon namespace /** * like du(1): return the number bytes used by a directory structure, counting hardlinked files only once * @param path File or directory to start counting recursively * @param error Will contain the error if the return value is -1 [optional] * @return size in bytes on success, -1 on error */ long long du(const std::string &path, std::string *error) { long long sum = 0; std::map > counted_inodes; try { du_internal(path, sum, counted_inodes); } catch (exception &e) { if (error) { ostringstream out; out << e.what() << " (" << strerror(errno) << ")"; *error = out.str(); } return -1; } catch (...) { if (error) { ostringstream out; out << "unknown error (" << strerror(errno) << ")"; *error = out.str(); } return -1; } return sum; } // eo du } // eo namespace I2n