173

I want to use scp to upload files but sometimes the target directory may not exist.

Is it possible to create the folder automatically? If so, how? If not, what alternative way can I try?

terdon
  • 242,166
AGamePlayer
  • 7,605

8 Answers8

111

This is one of the many things that rsync can do.

If you're using a version of rsync released in the past several years,¹ its basic command syntax is similar to scp

$ rsync -r local-dir remote-machine:path

That will copy local-source and its contents to $HOME/path/local-dir on the remote machine, creating whatever directories are required.³

rsync does have some restrictions here that can affect whether this will work in your particular situation. It won't create multiple levels of missing remote directories, for example; it will only create up to one missing level on the remote. You can easily get around this by preceding the rsync command with something like this:

$ ssh remote-host 'mkdir -p foo/bar/qux'

That will create the $HOME/foo/bar/qux tree if it doesn't exist. It won't complain or do anything else bad if it does already exist.

rsync sometimes has other surprising behaviors. Basically, you're asking it to figure out what you meant to copy, and its guesses may not match your assumptions. Try it and see. If it doesn't behave as you expect and you can't see why, post more details about your local and remote directory setups, and give the command you tried.


Footnotes:

  1. Before rsync 2.6.0 (1 Jan 2004), it required the -e ssh flag to make it behave like scp because it defaulted to the obsolete RSH protocol.

  2. scp and rsync share some flags, but there is only a bit of overlap.

  3. When using SSH as the transfer protocol, rsync uses the same defaults. So, just like scp, it will assume there is a user with the same name as your local user on the remote machine by default.

alkino
  • 103
Warren Young
  • 72,032
  • I am using Mac OSX, do I need to install rsync on both local and remote side? – AGamePlayer Mar 30 '15 at 13:27
  • searched rsync's documentation page, did not find how to use ssh-key to login. I don't want to use username/password to login – AGamePlayer Mar 30 '15 at 13:28
  • 2
    @AwQiruiGuo: Mac OS X ships with rsync. And yes, you do need rsync on the remote side, too, just like with scp. As for SSH keys, rsync is actually using SSH underneath with the default -e ssh, so if you have keys set up for use with scp, they will work for rsync as well. – Warren Young Mar 30 '15 at 13:29
  • 1
    @WarrenYoung -r makes no difference. The scenario you describe only works if you are copying a directory and if the target parent directory exists. So, rsync foo/ u@h:~/ will create the target directory foo but rsync foo/ u@h:~/bar/ will not create the target directory bar. That one will create bar/foo only if bar/ exists. In any case, the OP is uploading files and even in the cases where this works, it only works for entire directories. – terdon Mar 30 '15 at 13:29
  • @terdon: I just tried it here, and it works as I said. I have version 2.6.9 on the local machine and 3.1.0 on the remote. I said rsync -r local-dir remote.host.example.com:boo, and found boo/local-dir on the remote after an error-free transfer. boo did not exist before this. – Warren Young Mar 30 '15 at 13:36
  • @terdon: After some more experimentation, it appears to work for only a single directory level, and not with plain files. If you try rsync -r local-file remote.example.com:something and something is not a preexisting directory, it will assume you mean to do a file copy with a new name on the remote. When I tried to create more than one level of missing directories, I got a strange protocol error from rsync, though that may be due to the large mismatch in version numbers between my local and remote systems. – Warren Young Mar 30 '15 at 13:51
  • 1
    Yes, I've been playing with it too. It seems like specifying a file name also makes it fail. So rsync file host:/foo/file fails if foo/ doesn't exist but rsync file host:/foo/ creates the directory and places file in it. Strange. Anyway, you're quite right and the basic scenario works, I'll retract my downvote. – terdon Mar 30 '15 at 13:55
  • @WarrenYoung thanks, I have multiple keys on the remote side so I use scp -i ~/.ssh/particular-key to upload. What is the command for rsync? – AGamePlayer Mar 30 '15 at 14:17
  • @WarrenYoung one more question is that my local rsync is 2.6.9 while on the remote it's 3.0.9 - how do I upgrade my client side? (Mac OSX) – AGamePlayer Mar 30 '15 at 14:18
  • @AwQiruiGuo: Multiple keys on the remote side makes no sense. If the remote side will accept many keys and your default local key is one of them, then you don't need any special options. If you have multiple local keys and your default key won't work to get you into the remote machine, then you pass the ssh -i command to rsync -e, like rsync -e 'ssh -i ~/.ssh/some-other-key' .... – Warren Young Mar 30 '15 at 14:27
  • typo. on local, not remote. – AGamePlayer Mar 30 '15 at 14:34
  • @AwQiruiGuo: Regarding upgrading the Mac OS X version of rsync, I just successfully did it with Homebrew, saying brew install homebrew/dupes/rsync. However, doing so did not allow me to create 2+ levels of missing directories. See my edited answer for a coping strategy. – Warren Young Mar 30 '15 at 14:35
76

If you are copying a group of files, not really. If you are copying a directory and all the contents below it, yes. Given this command:

$ scp -pr /source/directory user@host:the/target/directory

If directory does not exist in ~/the/target on host, it will be created. If, however, ~the/target does not exist, you will likely have issues - I don't remember the exact results of this case, but be prepared for the intended scp to fail.

John
  • 17,011
  • 4
    scp: /home/madmike/test_bin: No such file or directory As soon as the target directory is missing this breaks on my setup. Using cygwin on the client and RHEL 6.9 on the server. What is your setup? – MadMike Feb 12 '18 at 15:00
  • @John, the same issue either with relative path or absolute. – yuklia Jan 23 '21 at 07:24
  • 6
    From man scp: -p Preserves modification times, access times, and modes from the original file. – Ricardo Jul 14 '21 at 23:02
  • 2
    This should be the accepted answer, since the OP asks how to do this with scp. The accepted answer does not answer the question of "how can I do this with scp?". Furthermore, from a practical POV, the accepted answer also failed for me when I was trying to do this on a machine that had rsync disabled (synology). – catchdave Oct 24 '22 at 05:29
36

As far as I know, scp itself cannot do that, no. However, you could just ssh to the target machine, create the directory and then copy. Something like:

ssh user@host "mkdir -p /target/path/" &&
    scp /path/to/source user@host:/target/path/

Note that if you are copying entire directories, the above is not needed. For example, to copy the directory ~/foo to the remote host, you could use the -r (recursive) flag:

scp -r ~/foo/ user@host:~/

That will create the target directory ~/foo on the remote host. However, it can't create the parent directory. scp ~/foo user@host:~/bar/foo will fail unless the target directory bar exists. In any case, the -r flag won't help create a target directory if you are copying individual files.

terdon
  • 242,166
5

I use the following function before transferring btrfs snapshots over ssh:

check_remote_dir() {
        printf "\ntesting remote directory: '$1' "

        if ssh -p $PORT $ROOT@$REMOTE "[ ! -d $1 ]"; then
                printf "\nCreating: $1 on $ROOT@$REMOTE\n"
                ssh -p $PORT $ROOT@$REMOTE "mkdir -p $1"
        else
                printf "[OK]\n"
        fi
}

Just call the function in your script with:

check_remote_dir /my/remote/path

  • I do pretty much the same thing, but I have to type in the password manually (for security reasons), so I end up typing it in at least twice... – Stefan Reich Dec 14 '18 at 12:25
3

Yes, you can. According to scp's man page:

man scp

.....

-r Recursively copy entire directories. Note that scp follows symâ bolic links encountered in the tree traversal.

....

YoMismo
  • 4,015
  • 11
    That will not create the target parent directory. It can copy a directory to an existing location. So, scp -r foo/ u@h:~/bar will create bar/foo if the remote bar/ exists but will fail if it doesn't. – terdon Mar 30 '15 at 13:32
  • 1
    It won't fail. It will copy foo contents to the new directory /bar. Of course the name has been changed which may not be what the OP wants. If what you want is a new tree structure created in the destination machine then scp won't do the job, if what you want is to replicate a tree structure from a certain branch in the tree then that structure is created. – YoMismo Mar 30 '15 at 13:46
  • 11
    Yes, but this only works if you are copying directories, not files. scp -r file host:~/bar/ won't create bar/. – terdon Mar 30 '15 at 13:57
1

When restricted to only scp or sftp with no ssh available (by rssh), it is possible to create directories by copying empty directory tree with rsync and then copying files.

That is how I put public ssh key to remote host when ssh-copy-id does not work and .ssh directory does not exists:

rsync -e 'ssh -p22' -av -f"+ */" -f"- *" ~/.ssh backup@1.2.3.4:~/
scp -P22 ~/.ssh/id_rsa.pub backup@1.2.3.4:~/.ssh/authorized_keys

It is possible to combine this commands into one rsync adding one more include filter in the middle, if target file name is the same.

1

How about "smart" scp + ssh mkdir?

This is a script to create target directory hierarchy only once per directory (not a blind ssh mkdir -p each time).

Rationale

I'm copying motion recordings (video files) as they are written to local disk (event-driven).

  • rsync doesn't make sense because there is no point in doing full directory comparisons - I am transferring one file per event. If I restrict rsync to just one subdir or file, then it won't be able to create the full target directory hierarchy.
  • scp will work great but it doesn't know if the target directory exists, and I don't want to run an ssh mkdir -p for every file.

Solution

  1. Create each target dir via ssh the first time there is a need (first time we are about to copy into it).
  2. Cache target dir name in a an array so that we won't ssh mkdir each time we see it again. Clear this array daily so that it doesn't get huge.
  3. scp the file.

Example code:

#!/bin/bash

TARGETHOST="some.hostname" TARGETPATH="/data/backups/MOTION/"

ensure_dir() { if [ "$TODAY" != "$(date +%D)" ] ; then CREATED=() # reset array daily and first time so it won't get huge TODAY=$(date +%D) fi
if [[ ! " ${CREATED[@]} " =~ " ${1} " ]]; then # create dir echo "Creating remote directory: $1" ssh $TARGETHOST "mkdir -p ${TARGETPATH}$1" CREATED+=("$1") fi
}

This line is specific to my use case, watching a directory for new files:

inotifywait -mr --format '%w %f' -e close_write --exclude '.jpg' /data/MOTION/ | while read filedir filename ; do RELDIR=$(echo $filedir | sed 's|/data/MOTION/||') ensure_dir "$RELDIR" echo "Copying '$filename' from '${filedir}' to '${RELDIR}'" scp -r -p "${filedir}${filename}" "${TARGETHOST}:${TARGETPATH}${RELDIR}${filename}" done

Typical output, illustrating "smart" mkdir behavior:

Creating remote directory: 2019-12-28/movie-C6/
Copying 'test1.mp4' from '/data/MOTION/2019-12-28/movie-C6/' to '2019-12-28/movie-C6/'
Creating remote directory: 2019-12-28/movie-C4/
Copying '095700.mp4' from '/data/MOTION/2019-12-28/movie-C4/' to '2019-12-28/movie-C4/'
Copying '095841.mp4' from '/data/MOTION/2019-12-28/movie-C4/' to '2019-12-28/movie-C4/'
Copying '100410.mp4' from '/data/MOTION/2019-12-28/movie-C4/' to '2019-12-28/movie-C4/'
Creating remote directory: 2019-12-28/movie-C2/
Copying '102106.mp4' from '/data/MOTION/2019-12-28/movie-C2/' to '2019-12-28/movie-C2/'
Creating remote directory: 2019-12-28/movie-C5/
Copying '102318.mp4' from '/data/MOTION/2019-12-28/movie-C5/' to '2019-12-28/movie-C5/'
Copying '102326.mp4' from '/data/MOTION/2019-12-28/movie-C2/' to '2019-12-28/movie-C2/'
Creating remote directory: 2019-12-28/movie-C1/
Copying '102450.mp4' from '/data/MOTION/2019-12-28/movie-C1/' to '2019-12-28/movie-C1/'
Copying '102744.mp4' from '/data/MOTION/2019-12-28/movie-C2/' to '2019-12-28/movie-C2/'
Copying '103008.mp4' from '/data/MOTION/2019-12-28/movie-C2/' to '2019-12-28/movie-C2/'
Copying '103945.mp4' from '/data/MOTION/2019-12-28/movie-C6/' to '2019-12-28/movie-C6/'
Copying '104252.mp4' from '/data/MOTION/2019-12-28/movie-C6/' to '2019-12-28/movie-C6/'
Copying '104824.mp4' from '/data/MOTION/2019-12-28/movie-C6/' to '2019-12-28/movie-C6/'
Akom
  • 191
0

You could chain the command with the mkdir command and then reference to the folder you've created:

mkdir ~/new-folder/ && scp -P 22 <remote/url>:~/new-folder/
Rui F Ribeiro
  • 56,709
  • 26
  • 150
  • 232
Ducky
  • 101