Skip to content

Commit

Permalink
Allow published repo to reside on different filesystem
Browse files Browse the repository at this point in the history
Currently packages are hardlinked from the internal pool to the
published repo, which requires them to reside on the same filesystem.
This precludes mounting the published repo from a remote machine,
e.g. via NFS or sshfs, which would be desirable to physically separate
the machine hosting the internal pool and signing key from the machine
serving the published repo.

Autodetect such a setup and, if found, copy files instead of using
hardlinks.

Add helpers to check whether two given paths reside on the same
filesystem and to check whether an already published package file
is identical to the one to be published by comparing their MD5 hashes.
  • Loading branch information
l1k committed Mar 24, 2017
1 parent 92c844b commit 72dd4ba
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 9 deletions.
36 changes: 27 additions & 9 deletions files/public.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"syscall"

"github.com/smira/aptly/aptly"
"github.com/smira/aptly/utils"
)

// PublishedStorage abstract file system with public dirs (published repos)
Expand Down Expand Up @@ -93,6 +94,11 @@ func (storage *PublishedStorage) LinkFromPool(publishedDirectory string, sourceP
return err
}

sameFilesystem, err := utils.SameFilesystem(sourcePath, poolPath)
if err != nil {
return err
}

var dstStat, srcStat os.FileInfo

dstStat, err = os.Stat(filepath.Join(poolPath, baseName))
Expand All @@ -104,15 +110,24 @@ func (storage *PublishedStorage) LinkFromPool(publishedDirectory string, sourceP
return err
}

srcSys := srcStat.Sys().(*syscall.Stat_t)
dstSys := dstStat.Sys().(*syscall.Stat_t)

// source and destination inodes match, no need to link
if srcSys.Ino == dstSys.Ino {
return nil
if sameFilesystem {
srcSys := srcStat.Sys().(*syscall.Stat_t)
dstSys := dstStat.Sys().(*syscall.Stat_t)

// source and destination inodes match, no need to link
if srcSys.Ino == dstSys.Ino {
return nil
}
} else {
destMD5, err := utils.MD5ChecksumForFile(filepath.Join(poolPath, baseName))

// identical destination file already exists, no need to copy
if err == nil && sourceMD5 == destMD5 {
return nil
}
}

// source and destination have different inodes, if !forced, this is fatal error
// source and destination differ, if !forced, this is fatal error
if !force {
return fmt.Errorf("error linking file to %s: file already exists and is different", filepath.Join(poolPath, baseName))
}
Expand All @@ -124,8 +139,11 @@ func (storage *PublishedStorage) LinkFromPool(publishedDirectory string, sourceP
}
}

// destination doesn't exist (or forced), create link
return os.Link(sourcePath, filepath.Join(poolPath, baseName))
// destination doesn't exist (or forced), create link or copy
if sameFilesystem {
return os.Link(sourcePath, filepath.Join(poolPath, baseName))
}
return utils.CopyFile(sourcePath, filepath.Join(poolPath, baseName))
}

// Filelist returns list of files under prefix
Expand Down
17 changes: 17 additions & 0 deletions utils/checksum.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,23 @@ import (
"os"
)

// MD5ChecksumForFile computes just the MD5 hash and not all the others
func MD5ChecksumForFile(path string) (string, error) {
file, err := os.Open(path)
if err != nil {
return "", err
}
defer file.Close()

hash := md5.New()
_, err = io.Copy(hash, file)
if err != nil {
return "", err
}

return fmt.Sprintf("%x", hash.Sum(nil)), nil
}

// ChecksumInfo represents checksums for a single file
type ChecksumInfo struct {
Size int64
Expand Down
25 changes: 25 additions & 0 deletions utils/samefilesystem.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package utils

import (
"os"
"syscall"
)

// SameFilesystem checks whether two existing paths reside on the same
// filesystem and can thus be hardlinked
func SameFilesystem(path1, path2 string) (bool, error) {
path1Stat, err := os.Stat(path1)
if err != nil {
return false, err
}

path2Stat, err := os.Stat(path2)
if err != nil {
return false, err
}

path1Sys := path1Stat.Sys().(*syscall.Stat_t)
path2Sys := path2Stat.Sys().(*syscall.Stat_t)

return path1Sys.Dev == path2Sys.Dev, nil
}

0 comments on commit 72dd4ba

Please sign in to comment.