Rename get_dir_size() to get_dir_count()
[libi2ncommon] / src / filefunc.cpp
index a00c3fe..e1e4882 100644 (file)
@@ -1,9 +1,27 @@
+/*
+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.
+*/
 /***************************************************************************
-                          escape.cpp  -  escaping of strings
+                          filefunc.cpp  -  functions for working with FS
                              -------------------
     begin                : Sun Nov 14 1999
     copyright            : (C) 1999 by Intra2net AG
-    email                : info@intra2net.com
  ***************************************************************************/
 
 #include <list>
 
 #include <sys/types.h>
 #include <sys/stat.h>
+#include <sys/statvfs.h>
 #include <dirent.h>
 #include <pwd.h>
 #include <grp.h>
 #include <unistd.h>
 #include <errno.h>
+#include <string.h>
+#include <map>
+#include <set>
 
 #include <boost/scoped_array.hpp>
+#include <boost/foreach.hpp>
 #include "filefunc.hxx"
 #include "stringfunc.hxx"
 
-
 namespace I2n
 {
 
@@ -36,7 +58,8 @@ using namespace std;
 */
 
 Stat::Stat()
-: Valid(false)
+    : FollowLinks(true)
+    , Valid(false)
 {
    clear();
 } // eo Stat::Stat()
@@ -62,7 +85,8 @@ void Stat::recheck()
 {
     if (! Path.empty())
     {
-        stat(Path, FollowLinks);
+        // pass a copy of Path: otherwise clear() would leave an empty reference
+        stat(string(Path), FollowLinks);
     }
 } // eo Stat::recheck()
 
@@ -95,6 +119,10 @@ void Stat::stat(const std::string& path, bool follow_links)
         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 );
@@ -123,6 +151,7 @@ void Stat::clear()
     Gid    = 0;
     DeviceType = 0;
     Size   = 0;
+    BytesOnDisk = 0;
     Atime  = 0;
     Mtime  = 0;
     Ctime  = 0;
@@ -234,6 +263,40 @@ time_t file_mtime(const std::string& path)
 
 
 /**
+ * @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.
@@ -245,13 +308,14 @@ bool get_dir(
     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 *entry;
-    while ( NULL != (entry = ::readdir(dir)) )
+    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] == '.') )
@@ -279,11 +343,37 @@ std::vector< std::string > get_dir(const std::string& path, bool include_dot_nam
 } // 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 iff the unlink was successful.
+ * @return @a true if the unlink was successful.
  */
 bool unlink( const std::string& path )
 {
@@ -421,13 +511,19 @@ std::string read_file(const std::string& path)
  * @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 write_file(const std::string& path, const std::string& data, bool trunc)
 {
-    std::ofstream f( path.c_str(), std::ios::out | std::ios::binary | std::ios::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() );
@@ -441,6 +537,66 @@ bool write_file(const std::string& path, const std::string& data)
 
 
 /**
+ * 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.
@@ -554,6 +710,25 @@ std::string normalize_path(const std::string& path)
 
 
 /**
+ * @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.
@@ -591,57 +766,56 @@ bool chown(const std::string& path, const I2n::User& user, const I2n::Group& gro
 /**
  * Recursive delete of files and directories
  * @param path File or directory to delete
- * @param error Will contain the error if the return value is false
+ * @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, std::string *error)
+bool recursive_delete(const std::string &path,
+                      bool keep_parent_dir,
+                      std::string *error)
 {
     bool rtn = true;
 
-    try {
-        struct stat my_stat;
-        if (stat(path.c_str(), &my_stat) != 0) {
+    try
+    {
+        Stat sp(path, false);
+        if (!sp)
             throw runtime_error("can't stat " + path);
-        }
-
-        if (S_ISDIR(my_stat.st_mode)) {
-            DIR *dir = opendir(path.c_str());
-            if (!dir) {
-                throw runtime_error("can't open directory " + path);
-            }
-
-            struct dirent *entry;
-            while ((entry = readdir(dir))) {
-                string filename = entry->d_name;
-                if (filename == "." || filename == "..") {
-                    continue;
-                }
 
+        if (sp.is_directory())
+        {
+            std::vector<std::string> dirents = get_dir(path, false);
+            BOOST_FOREACH(const std::string &filename, dirents)
+            {
                 // Delete subdir or file.
-                rtn = recursive_delete(path + "/" + filename, error);
-                if (rtn == false) {
+                rtn = recursive_delete(path + "/" + filename, false, error);
+                if (rtn == false)
                     break;
-                }
             }
 
-            closedir(dir);
-            if (rmdir(path.c_str()) != 0) {
+            if (keep_parent_dir == false && !rmdir(path))
                 throw runtime_error("can't remove directory " + path);
-            }
-        } else {
-            if (unlink(path.c_str()) != 0) {
+        }
+        else
+        {
+            if (!unlink(path))
                 throw runtime_error("can't unlink " + path);
-            }
         }
-    } catch (exception &e) {
-        if (error) {
+    }
+    catch (exception &e)
+    {
+        if (error)
+        {
             ostringstream out;
             out << e.what() << " (" << strerror(errno) << ")";
             *error = out.str();
         }
         rtn = false;
-    } catch (...) {
-        if (error) {
+    }
+    catch (...)
+    {
+        if (error)
+        {
             ostringstream out;
             out << "unknown error (" << strerror(errno) << ")";
             *error = out.str();
@@ -652,4 +826,280 @@ bool recursive_delete(const std::string &path, std::string *error)
     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<char> 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<std::string> &keep_files,
+                           const std::string &prefix)
+{
+    std::vector<std::string> 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<dev_t, std::set<ino_t> > &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<std::string> 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<dev_t, std::set<ino_t> > 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