diff --git a/src/fsys/dir.go b/src/fsys/dir.go index 6140aba..3e77ffd 100644 --- a/src/fsys/dir.go +++ b/src/fsys/dir.go @@ -33,6 +33,7 @@ type Directory struct { Path string Size uint64 RelativeParentPath string // Relative path to the directory, where the highest point in the hierarchy is the upmost parent dir. Set manually + Symlinks []*Symlink Files []*File Directories []*Directory } @@ -70,6 +71,7 @@ func GetDir(path string, recursive bool) (*Directory, error) { var innerDirs []*Directory var innerFiles []*File + var innerSymlinks []*Symlink for _, entry := range entries { entryInfo, err := entry.Info() if err != nil { @@ -93,22 +95,42 @@ func GetDir(path string, recursive bool) (*Directory, error) { // if not - skip the directory and only work with the files } else { - innerFilePath := filepath.Join(absPath, entryInfo.Name()) + // not a directory - innerFile, err := GetFile(innerFilePath) - if err != nil { - // skip this file - continue - } + switch entryInfo.Mode()&os.ModeSymlink != 0 { + case true: + // it is a symlink + innerSymlinkPath := filepath.Join(absPath, entryInfo.Name()) + + symlink, err := GetSymlink(innerSymlinkPath, false) + if err != nil { + // skip this symlink + continue + } + + innerSymlinks = append(innerSymlinks, symlink) - directory.Size += innerFile.Size + case false: + // it is a usual file + + innerFilePath := filepath.Join(absPath, entryInfo.Name()) + + innerFile, err := GetFile(innerFilePath) + if err != nil { + // skip this file + continue + } - innerFiles = append(innerFiles, innerFile) + directory.Size += innerFile.Size + + innerFiles = append(innerFiles, innerFile) + } } } directory.Directories = innerDirs directory.Files = innerFiles + directory.Symlinks = innerSymlinks return &directory, nil } @@ -134,7 +156,27 @@ func (dir *Directory) GetAllFiles(recursive bool) []*File { return files } -// Sets `RelativeParentPath` relative to the given base path. +// Returns every symlink in that directory +func (dir *Directory) GetAllSymlinks(recursive bool) []*Symlink { + var symlinks []*Symlink = dir.Symlinks + + if recursive { + if len(dir.Directories) == 0 { + return symlinks + } + + for _, innerDir := range dir.Directories { + innerSymlinks := innerDir.GetAllSymlinks(recursive) + symlinks = append(symlinks, innerSymlinks...) + } + } else { + symlinks = dir.Symlinks + } + + return symlinks +} + +// Sets `RelativeParentPath` relative to the given base path for files and `Path`, `TargetPath` for symlinks so the // file with such path: // /home/user/directory/somefile.txt // had a relative path like that: @@ -150,5 +192,20 @@ func (dir *Directory) SetRelativePaths(base string, recursive bool) error { file.RelativeParentPath = relPath } + + for _, symlink := range dir.GetAllSymlinks(recursive) { + symRelPath, err := filepath.Rel(base, symlink.Path) + if err != nil { + return err + } + symlink.Path = symRelPath + + symRelTargetPath, err := filepath.Rel(base, symlink.TargetPath) + if err != nil { + return err + } + symlink.TargetPath = symRelTargetPath + } + return nil } diff --git a/src/fsys/dir_test.go b/src/fsys/dir_test.go index 76904a3..c798824 100644 --- a/src/fsys/dir_test.go +++ b/src/fsys/dir_test.go @@ -21,6 +21,8 @@ along with this program. If not, see . package fsys import ( + "os" + "path/filepath" "testing" ) @@ -79,25 +81,23 @@ func Test_GetFiles(t *testing.T) { } -// func Test_SetRelativePaths(t *testing.T) { -// dirpath := "../testfiles/" +func Test_GetSymlinks(t *testing.T) { + dirpath := "../testfiles/" -// dir, err := GetDir(dirpath, true) -// if err != nil { -// t.Fatalf("%s", err) -// } + os.Symlink(filepath.Join(dirpath, "testfile.txt"), filepath.Join(dirpath, "testsymlink.txt")) + os.Symlink(filepath.Join(dirpath, "testdir", "testfile2.txt"), filepath.Join(dirpath, "testdir", "testsymlink2.txt")) -// absDirPath, err := filepath.Abs(dirpath) -// if err != nil { -// t.Fatalf("%s", err) -// } + dir, err := GetDir(dirpath, true) + if err != nil { + t.Fatalf("%s", err) + } -// err = dir.SetRelativePaths(absDirPath, true) -// if err != nil { -// t.Fatalf("%s", err) -// } + // recursive + symlinks := dir.GetAllSymlinks(true) + + symlinkCount := 2 -// for count, file := range dir.GetAllFiles(true) { -// t.Errorf("[%d] %v\n", count, file.RelativeParentPath) -// } -// } + if len(symlinks) != symlinkCount { + t.Fatalf("expected to get %d symlinks; got %d\n", symlinkCount, len(symlinks)) + } +} diff --git a/src/fsys/symlink.go b/src/fsys/symlink.go new file mode 100644 index 0000000..34d90e2 --- /dev/null +++ b/src/fsys/symlink.go @@ -0,0 +1,70 @@ +/* +ftu - file transferring utility. +Copyright (C) 2021,2022 Kasyanov Nikolay Alexeevich (Unbewohnte (https://unbewohnte.xyz/)) + +This file is a part of ftu + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +package fsys + +import ( + "fmt" + "os" +) + +type Symlink struct { + TargetPath string + Path string +} + +// Checks whether path is referring to a symlink or not +func IsSymlink(path string) (bool, error) { + stats, err := os.Lstat(path) + if err != nil { + return false, err + } + isSymlink := stats.Mode()&os.ModeSymlink != 0 + + return isSymlink, nil +} + +var ErrorNotSymlink error = fmt.Errorf("not a symlink") + +// get necessary information about a symlink in a filesystem. If check is false - +// does not check if path REALLY refers to a symlink +func GetSymlink(path string, check bool) (*Symlink, error) { + if check { + isSymlink, err := IsSymlink(path) + if err != nil { + return nil, err + } + if !isSymlink { + return nil, ErrorNotSymlink + } + } + + target, err := os.Readlink(path) + if err != nil { + return nil, err + } + + symlink := Symlink{ + TargetPath: target, + Path: path, + } + + return &symlink, nil +} diff --git a/src/fsys/symlink_test.go b/src/fsys/symlink_test.go new file mode 100644 index 0000000..deb0e06 --- /dev/null +++ b/src/fsys/symlink_test.go @@ -0,0 +1,22 @@ +package fsys + +import ( + "os" + "path/filepath" + "testing" +) + +func Test_IsSymlink(t *testing.T) { + dirpath := "../testfiles/" + + symlinkPath := filepath.Join(dirpath, "testsymlink.txt") + os.Symlink(filepath.Join(dirpath, "testfile.txt"), symlinkPath) + + isSymlink, err := IsSymlink(symlinkPath) + if err != nil { + t.Fatalf("%s\n", err) + } + if !isSymlink { + t.Fatalf("%s expected to be a symlink\n", symlinkPath) + } +} diff --git a/src/main.go b/src/main.go index bb17ea7..f258926 100644 --- a/src/main.go +++ b/src/main.go @@ -30,7 +30,7 @@ import ( ) var ( - VERSION string = "v2.2.3" + VERSION string = "v2.3.0" versionInformation string = fmt.Sprintf("ftu %s\nfile transferring utility\n\nCopyright (C) 2021,2022 Kasyanov Nikolay Alexeevich (Unbewohnte (me@unbewohnte.xyz))\nThis program comes with ABSOLUTELY NO WARRANTY.\nThis is free software, and you are welcome to redistribute it under certain conditions; type \"ftu -l\" for details.\n", VERSION) diff --git a/src/node/node.go b/src/node/node.go index 68334fb..503fd7e 100644 --- a/src/node/node.go +++ b/src/node/node.go @@ -49,16 +49,18 @@ type netInfo struct { // Sending-side node information type sending struct { - ServingPath string // path to the thing that will be sent - IsDirectory bool // is ServingPath a directory - Recursive bool // recursively send directory - CanSendBytes bool // is the other node ready to receive another piece - AllowedToTransfer bool // the way to notify the mainloop of a sending node to start sending pieces of files - InTransfer bool // already transferring|receiving files - FilesToSend []*fsys.File - CurrentFileID uint64 // an id of a file that is currently being transported - SentBytes uint64 // how many bytes sent already - TotalTransferSize uint64 // how many bytes will be sent in total + ServingPath string // path to the thing that will be sent + IsDirectory bool // is ServingPath a directory + Recursive bool // recursively send directory + CanSendBytes bool // is the other node ready to receive another piece + AllowedToTransfer bool // the way to notify the mainloop of a sending node to start sending pieces of files + InTransfer bool // already transferring|receiving files + FilesToSend []*fsys.File + SymlinksToSend []*fsys.Symlink + CurrentFileID uint64 // an id of a file that is currently being transported + SentBytes uint64 // how many bytes sent already + TotalTransferSize uint64 // how many bytes will be sent in total + CurrentSymlinkIndex uint64 // current index of a symlink that is } // Receiving-side node information @@ -355,8 +357,10 @@ func (node *Node) send() { panic(err) } filesToSend := DIRTOSEND.GetAllFiles(node.transferInfo.Sending.Recursive) + symlinksToSend := DIRTOSEND.GetAllSymlinks(node.transferInfo.Sending.Recursive) + + node.transferInfo.Sending.SymlinksToSend = symlinksToSend - // notify the other node about all the files that are going to be sent for counter, file := range filesToSend { // assign ID and add it to the node sendlist file.ID = uint64(counter) @@ -410,7 +414,14 @@ func (node *Node) send() { // Transfer section - if len(node.transferInfo.Sending.FilesToSend) == 0 { + // if all files have been sent -> send symlinks + if len(node.transferInfo.Sending.FilesToSend) == 0 && node.transferInfo.Sending.CurrentSymlinkIndex < uint64(len(node.transferInfo.Sending.SymlinksToSend)) { + protocol.SendSymlink(node.transferInfo.Sending.SymlinksToSend[node.transferInfo.Sending.CurrentSymlinkIndex], node.netInfo.Conn, encrKey) + node.transferInfo.Sending.CurrentSymlinkIndex++ + continue + } + + if len(node.transferInfo.Sending.FilesToSend) == 0 && node.transferInfo.Sending.CurrentSymlinkIndex == uint64(len(node.transferInfo.Sending.SymlinksToSend)) { // if there`s nothing else to send - create and send DONE packet protocol.SendPacket(node.netInfo.Conn, protocol.Packet{ Header: protocol.HeaderDone, @@ -857,6 +868,41 @@ func (node *Node) receive() { fmt.Printf("\nGot an encryption key: %s", encrKey) + case protocol.HeaderSymlink: + // SYMLINK~(string size in binary)(location in the filesystem)(string size in binary)(location of a target) + packetReader := bytes.NewReader(incomingPacket.Body) + + // extract the location of the symlink + var locationSize uint64 + binary.Read(packetReader, binary.BigEndian, &locationSize) + + symlinkLocationBytes := make([]byte, locationSize) + packetReader.Read(symlinkLocationBytes) + + // extract the target of a symlink + var targetSize uint64 + binary.Read(packetReader, binary.BigEndian, &targetSize) + + symlinkTargetLocationBytes := make([]byte, targetSize) + packetReader.Read(symlinkTargetLocationBytes) + + symlinkLocation := string(symlinkLocationBytes) + symlinkTargetLocation := string(symlinkTargetLocationBytes) + + // create a symlink + + // should be already downloaded + symlinkDir := filepath.Join(node.transferInfo.Receiving.DownloadsPath, filepath.Dir(symlinkLocation)) + os.MkdirAll(symlinkDir, os.ModePerm) + + os.Symlink( + filepath.Join(node.transferInfo.Receiving.DownloadsPath, symlinkTargetLocation), + filepath.Join(node.transferInfo.Receiving.DownloadsPath, symlinkLocation)) + + protocol.SendPacket(node.netInfo.Conn, protocol.Packet{ + Header: protocol.HeaderReady, + }) + case protocol.HeaderDone: node.mutex.Lock() node.stopped = true diff --git a/src/protocol/headers.go b/src/protocol/headers.go index 17d336f..0bb8e47 100644 --- a/src/protocol/headers.go +++ b/src/protocol/headers.go @@ -114,3 +114,10 @@ const HeaderDirectory Header = "DIRECTORY" // Body must contain a file ID. // ie: ALREADYHAVE~(file ID in binary) const HeaderAlreadyHave Header = "ALREADYHAVE" + +// SYMLINK +// Sent by sender AFTER ALL FILES has been sent already. Indicates that there +// is a symlink in some place that points to some other already received file. +// Body must contain information where the symlink is and the target file. +// ie: SYMLINK~(string size in binary)(location in the filesystem)(string size in binary)(location of a target) +const HeaderSymlink Header = "SYMLINK" diff --git a/src/protocol/send.go b/src/protocol/send.go index 27dba0e..9beeabc 100644 --- a/src/protocol/send.go +++ b/src/protocol/send.go @@ -209,3 +209,36 @@ func SendPiece(file *fsys.File, connection net.Conn, encrKey []byte) (uint64, er return sentBytes, nil } + +// Sends a symlink to the other side. If encrKey is not nil - encrypts the packet with this key +func SendSymlink(symlink *fsys.Symlink, connection net.Conn, encrKey []byte) error { + symlinkPacket := Packet{ + Header: HeaderSymlink, + } + + symlinkPacketBodyBuff := new(bytes.Buffer) + + // SYMLINK~(string size in binary)(location in the filesystem)(string size in binary)(location of a target) + + binary.Write(symlinkPacketBodyBuff, binary.BigEndian, uint64(len(symlink.Path))) + symlinkPacketBodyBuff.Write([]byte(symlink.Path)) + + binary.Write(symlinkPacketBodyBuff, binary.BigEndian, uint64(len(symlink.TargetPath))) + symlinkPacketBodyBuff.Write([]byte(symlink.TargetPath)) + + symlinkPacket.Body = symlinkPacketBodyBuff.Bytes() + + if encrKey != nil { + err := symlinkPacket.EncryptBody(encrKey) + if err != nil { + return err + } + } + + err := SendPacket(connection, symlinkPacket) + if err != nil { + return err + } + + return nil +} diff --git a/src/testfiles/testdir/testsymlink2.txt b/src/testfiles/testdir/testsymlink2.txt new file mode 120000 index 0000000..2ed3a5a --- /dev/null +++ b/src/testfiles/testdir/testsymlink2.txt @@ -0,0 +1 @@ +../testfiles/testdir/testfile2.txt \ No newline at end of file diff --git a/src/testfiles/testsymlink.txt b/src/testfiles/testsymlink.txt new file mode 120000 index 0000000..c6a254b --- /dev/null +++ b/src/testfiles/testsymlink.txt @@ -0,0 +1 @@ +../testfiles/testfile.txt \ No newline at end of file