fs-util: add new helper linkat_replace()
This commit is contained in:
@@ -1256,3 +1256,79 @@ int link_fd(int fd, int newdirfd, const char *newpath) {
|
||||
|
||||
return RET_NERRNO(linkat(fd, "", newdirfd, newpath, AT_EMPTY_PATH));
|
||||
}
|
||||
|
||||
int linkat_replace(int olddirfd, const char *oldpath, int newdirfd, const char *newpath) {
|
||||
_cleanup_close_ int old_fd = -EBADF;
|
||||
int r;
|
||||
|
||||
assert(olddirfd >= 0 || olddirfd == AT_FDCWD);
|
||||
assert(newdirfd >= 0 || newdirfd == AT_FDCWD);
|
||||
assert(!isempty(newpath)); /* source path is optional, but the target path is not */
|
||||
|
||||
/* Like linkat() but replaces the target if needed. Is a NOP if source and target already share the
|
||||
* same inode. */
|
||||
|
||||
if (olddirfd == AT_FDCWD && isempty(oldpath)) /* Refuse operating on the cwd (which is a dir, and dirs can't be hardlinked) */
|
||||
return -EISDIR;
|
||||
|
||||
if (path_implies_directory(oldpath)) /* Refuse these definite directories early */
|
||||
return -EISDIR;
|
||||
|
||||
if (path_implies_directory(newpath))
|
||||
return -EISDIR;
|
||||
|
||||
/* First, try to link this directly */
|
||||
if (oldpath)
|
||||
r = RET_NERRNO(linkat(olddirfd, oldpath, newdirfd, newpath, 0));
|
||||
else
|
||||
r = link_fd(olddirfd, newdirfd, newpath);
|
||||
if (r >= 0)
|
||||
return 0;
|
||||
if (r != -EEXIST)
|
||||
return r;
|
||||
|
||||
old_fd = xopenat(olddirfd, oldpath, O_PATH|O_CLOEXEC);
|
||||
if (old_fd < 0)
|
||||
return old_fd;
|
||||
|
||||
struct stat old_st;
|
||||
if (fstat(old_fd, &old_st) < 0)
|
||||
return -errno;
|
||||
|
||||
if (S_ISDIR(old_st.st_mode)) /* Don't bother if we are operating on a directory */
|
||||
return -EISDIR;
|
||||
|
||||
struct stat new_st;
|
||||
if (fstatat(newdirfd, newpath, &new_st, AT_SYMLINK_NOFOLLOW) < 0)
|
||||
return -errno;
|
||||
|
||||
if (S_ISDIR(new_st.st_mode)) /* Refuse replacing directories */
|
||||
return -EEXIST;
|
||||
|
||||
if (stat_inode_same(&old_st, &new_st)) /* Already the same inode? Then shortcut this */
|
||||
return 0;
|
||||
|
||||
_cleanup_free_ char *tmp_path = NULL;
|
||||
r = tempfn_random(newpath, /* extra= */ NULL, &tmp_path);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = link_fd(old_fd, newdirfd, tmp_path);
|
||||
if (r < 0) {
|
||||
if (!ERRNO_IS_PRIVILEGE(r))
|
||||
return r;
|
||||
|
||||
/* If that didn't work due to permissions then go via the path of the dentry */
|
||||
r = RET_NERRNO(linkat(olddirfd, oldpath, newdirfd, tmp_path, 0));
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
r = RET_NERRNO(renameat(newdirfd, tmp_path, newdirfd, newpath));
|
||||
if (r < 0) {
|
||||
(void) unlinkat(newdirfd, tmp_path, /* flags= */ 0);
|
||||
return r;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -148,3 +148,5 @@ static inline int xopenat_lock(int dir_fd, const char *path, int open_flags, Loc
|
||||
}
|
||||
|
||||
int link_fd(int fd, int newdirfd, const char *newpath);
|
||||
|
||||
int linkat_replace(int olddirfd, const char *oldpath, int newdirfd, const char *newpath);
|
||||
|
||||
Reference in New Issue
Block a user