43

Like many others I've seen in the Googleverse, I fell victim to the File.exists? trap, which of course checks your local file system, not the server you are deploying to.

I found one result that used a shell hack like:

if [[ -d #{shared_path}/images ]]; then ...

but that doesn't sit well with me, unless it were wrapped nicely in a Ruby method.

Has anybody solved this elegantly?

czerasz
  • 13,682
  • 9
  • 53
  • 63
Teflon Ted
  • 8,696
  • 19
  • 64
  • 78

5 Answers5

61

In capistrano 3, you can do:

on roles(:all) do
  if test("[ -f /path/to/my/file ]")
    # the file exists
  else
    # the file does not exist
  end
end

This is nice because it returns the result of the remote test back to your local ruby program and you can work in simpler shell commands.

Matt Connolly
  • 9,757
  • 2
  • 65
  • 61
  • Many thanks! I ended up with something like `unless test("[ -f " + shared_path.to_s + "/a_shared_file.txt ]" )` – Lightheaded Oct 28 '14 at 14:01
  • 2
    Matt, can you link to the docs for `test`? It's a hard word to search for. Thanks! – Jared Beck Mar 19 '15 at 21:21
  • 2
    FAQ Example: http://capistranorb.com/documentation/faq/how-can-i-check-for-existing-remote-file/ – Jared Beck Mar 19 '15 at 21:29
  • note that within context is not going to work here, so you'll need to test with absolute path. in case you want to check for directory existence, use `[ -p ... ]` – Evgenia Karunus Oct 23 '15 at 00:44
49

@knocte is correct that capture is problematic because normally everyone targets deployments to more than one host (and capture only gets the output from the first one). In order to check across all hosts, you'll need to use invoke_command instead (which is what capture uses internally). Here is an example where I check to ensure a file exists across all matched servers:

def remote_file_exists?(path)
  results = []

  invoke_command("if [ -e '#{path}' ]; then echo -n 'true'; fi") do |ch, stream, out|
    results << (out == 'true')
  end

  results.all?
end

Note that invoke_command uses run by default -- check out the options you can pass for more control.

Patrick Reagan
  • 910
  • 7
  • 7
  • everyone, please upvote this and downvote the highly-voted answer, there cannot be such a huge mistake in stackoverflow! – knocte Mar 15 '13 at 15:39
  • 1
    isn't your final condition going to fail if you have more than on target? the results array could be [true, true, true]. i think you want to be using "results.all?" instead. – Teflon Ted Apr 03 '13 at 19:13
  • @TeflonTed -- yes you're correct. I updated the answer to reflect this change. Thanks! – Patrick Reagan Jul 25 '13 at 18:14
  • 4
    It seems to me that you ought to be doing `echo -n 'false';` in the `else` case... – Richard Cook Jul 31 '13 at 05:42
  • I agree with @RichardCook, without an else branch, results is empty and `[].all?` is false. On my system you only enter the invoke_command block of something it output. – Peter Long Aug 02 '13 at 16:35
  • @PeterLong: Interestingly, `[].all?` reports `true` on my system (Ruby 1.9.3). Which version of Ruby are you using? – Richard Cook Aug 02 '13 at 17:13
  • @RichardCook: You are correct. I have no idea how I came to that conclusion. Sorry for spreading miss information. Also, sorry about taking a month to notice. – Peter Long Sep 06 '13 at 00:54
  • How do I call this function? If I include it in my `deploy.rb`, I get an `undefined method 'remote_file_exists' for # (NoMethodError)` – basically just calling like `sudo "…" if remote_file_exists? "/foo/bar"` inside a task in a namespace. – slhck Sep 25 '13 at 08:03
  • @slhck, I think you're missing the '?' at the end of the method name. You should be able to put it anywhere in the file. – Isaac Betesh Dec 20 '13 at 15:33
  • @PeterLong, same here -- [].all? == true. I added !results.empty? to the return value to address this – Isaac Betesh Dec 20 '13 at 15:36
  • As already suggestest the cmd must be fixed to "if [ -e '#{path}' ]; then echo -n 'true'; else echo -n 'false'; fi" otherwise it does not work if you have a newly added server. In my case result was [true, true, true] for 4 servers but should be [true, true, true, false]. – Mr. Ronald May 12 '14 at 10:19
20

Inspired by @bhups response, with tests:

def remote_file_exists?(full_path)
  'true' ==  capture("if [ -e #{full_path} ]; then echo 'true'; fi").strip
end

namespace :remote do
  namespace :file do
    desc "test existence of missing file"
    task :missing do
      if remote_file_exists?('/dev/mull')
        raise "It's there!?"
      end
    end

    desc "test existence of present file"
    task :exists do
      unless remote_file_exists?('/dev/null')
        raise "It's missing!?"
      end
    end
  end
end
Teflon Ted
  • 8,696
  • 19
  • 64
  • 78
  • 7
    PEOPLE! capture() function only retrieves data from the first server, so please don't base any logic on this!! capistrano is multi-server – knocte Feb 24 '13 at 01:34
  • 1
    @knocte -- thanks for pointing that out, I was able to come up with a solution that works across all matched servers. See my answer below. – Patrick Reagan Mar 15 '13 at 15:29
  • COOL!! everyone please downvote this response and upvote Patrick's!!! this is a complete failure – knocte Mar 15 '13 at 15:33
5

May be you want to do is:

isFileExist = 'if [ -d #{dir_path} ]; then echo "yes"; else echo "no"; fi'.strip
puts "File exist" if isFileExist == "yes"
Damien MATHIEU
  • 31,924
  • 13
  • 86
  • 94
bhups
  • 14,345
  • 8
  • 49
  • 57
  • Thanks. I'm assuming you mean to wrap that with the "capture" method? http://www.capify.org/index.php/Capture – Teflon Ted Nov 02 '09 at 15:15
  • there is another way in ruby to capture the output: use the backticks: isFileExist = ` if [ -d #{dir_path} ]; then echo "yes"; else echo "no"; fi `.strip (make sure to drop the extra spaces inside the backticks: I have added them to aid the SO displaying) – D_K Feb 11 '15 at 10:28
4

I have done that before using the run command in capistrano (which execute a shell command on the remote server)

For example here is one capistrano task which will check if a database.yml exists in the shared/configs directory and link it if it exists.

  desc "link shared database.yml"
  task :link_shared_database_config do
    run "test -f #{shared_path}/configs/database.yml && ln -sf 
    #{shared_path}/configs/database.yml #{current_path}/config/database.yml || 
    echo 'no database.yml in shared/configs'"
  end
Aurélien Bottazini
  • 3,249
  • 17
  • 26