1

We have a program people can compile on their machines. It has an HTTP interface but can also be invoked by command line.

In order to provide some nice-looking error pages for HTTP clients, we want to provide error pages. We are using a very simple solution with go's html/template package.

So in order for the program to find the templates, we currently do:

func init() {
  prefStr := "path/to/http/tmpl"
  pathPrefix,err := filepath.Abs(prefStr)
  if err != nil {
    log.Warn("Template path %s is not available!", prefStr)
  }

  pathPrefix + "/err.html"
}

Now when debugging the app, this usually works well - we are in the package's root directory, so filepath.Abs() resolves correctly, like so: $GOPATH/github.com/user/repo/path/to/http/tmpl (expanding $GOPATH correctly)

But when we invoke the app via executable from the command line, this doesn't work. The command line could of course be invoked from anywhere on the filesystem, for convenience for example to provide a file in the current directory as a parameter.

In short, running /some/other/path/on/fs/our-executable filename.txt results in the init() function above breaking due to wrong concatenation of the directory: it takes /some/other/path/on/fs/ to create the absolute path, which is wrong. Thus it crashes with panic: open /some/other/path/on/fs/path/to/http/tmpl/err.html: no such file or directory

I've searched and so far only found this: How can I open files using relative paths in Go?

But this exactly doesn't apply for us. Another solution proposes to bundle compiled go resources, but this seems rather odd as the error pages are html text.

We've also tried https://stackoverflow.com/a/31464648/169252

but it has the same effect.

How can we make sure that the paths are always resolved correctly? This seems rather something which shouldn't be too difficult to be done, but we haven't managed so far.

EDIT: This is not an exact duplicate of the question How can I open files using relative paths in Go?. As already mentioned in my question text, I already had looked it up myself. It suggests to use filepath.Abs(). But as laid out in my question, for us this doesn't work, as if our executable is called from different places, filepath.Abs() doesn't return the same value, and thus doesn't work for us.

transient_loop
  • 5,984
  • 15
  • 58
  • 117
  • Is `path/to/http/tmpl` an absolute path here? Meaning it starts from the root of the file system. Example `/home/faboolous/abc/defg/tmpl` – Samuel Toh Aug 09 '17 at 00:10
  • @SamuelToh no, it is not an absolute path, otherwise it couldn't be portable. It's a relative path from the root of the repo. – transient_loop Aug 09 '17 at 00:27
  • use [go-bindata](https://github.com/jteeuwen/go-bindata) to compile the templates into the go binary – Mark Aug 09 '17 at 03:00
  • btw `go-bindata` is a dead project now. Have to refer to its fork for updates. Anyway, baking it into the binary has a disadvantage as this means that user cannot perform manual modification to the template files. – Samuel Toh Aug 09 '17 at 03:51
  • 1
    Possible duplicate of [How can I open files using relative paths in Go?](https://stackoverflow.com/questions/17071286/how-can-i-open-files-using-relative-paths-in-go) – TehSphinX Aug 09 '17 at 08:00
  • @Mark you may want to provide your suggestion as an answer; I currently believe it's the only way to really get it fail-proof – transient_loop Aug 10 '17 at 16:51

4 Answers4

0

I think your challenge here is that people can install the program anywhere on the disk and the program will have to be smart enough to know where it is later on.

One of the common approach that I have seen is that people typically use environment variables to anchor them to the application's installation path. I believe you may have seen environment variables with naming pattern of *_HOME like JAVA_HOME, MAVEN_HOME and their values are always filepath to the installation place.

I guess you can do the same here. Force your users to have MYAPP_HOME variable define and at the start of the application make sure that it is set or else throw an error saying MYAPP_HOME is not set.

Then all you need to do would be a simple lookup of the value for MYAPP_HOME + /http/tmpl to source for the template html files.

Example:

package main

import "os"

func main() {
    // Assuming MYAPP_HOME has been verified that it is set
    // Then:
    tmlPath := os.Getenv("MYAPP_HOME") + "/http/tmpl/"
    errTml := tmlPath + "err.html"
}
Samuel Toh
  • 18,006
  • 3
  • 24
  • 39
  • I understand this solution, thanks for suggesting. Isn't there really no way in Go to get the current directory the file is stored on? – transient_loop Aug 09 '17 at 15:09
  • I don't think there is because the binaries of your product can be installed anywhere on the filesystem and there is no way you can predict upfront where the user is going to put them unless you explicitly ask them to install it to a place. Then you can `hardcore` the path into your code which is not nice. – Samuel Toh Aug 10 '17 at 00:32
  • I'm thinking even if there is an API in `go` which can magically search for a file with name `tmpl.html` on a user's system, then the next problem becomes how do we verify that the tmpl file is indeed the one we are after? Because filename is not unqiue globally. Well we could do `checksum` but maybe it is a bit over kill and is definitely to make performance suffer... – Samuel Toh Aug 10 '17 at 00:34
0

If you're not keen on using the current working directory, or passing the directory in, you can find the absolute executable path by calling os.Executable from the os package.

appPath, err := os.Executable()

The os package will generally contain os specific stuff like how to get the current working directory. It's worth looking through the pkg docs and list of packages at golang.org if your'e ever stuck, as they are pretty good typically you'll find an answer there.

https://golang.org/pkg/os

Another approach you can take here if users install with go get is to rely on the fact that your templates will be installed with the pkg under GOPATH, so you can always find them at $GOPATH/src/your/project/path/templates (or ~/go the default gopath now that it is not strictly required).

The safest way is probably to bundle them with the binary in a virtual file system as this means you depend on nothing external and don't care where your app is hosted or even if it has access to files at all.

Kenny Grant
  • 9,360
  • 2
  • 33
  • 47
0

I recommend using a relative path in this case.

According to your description, it seems like you are developing a web application. While it works fine on individual developer's machine, you need to be mindful that your application can be deployed under any directory on your production server. You cannot determine where you app is going to be deployed, but you can always determine where static files are relative to the root directory of your app.

When you invoke your app in the command line, you should have all the required static files copied to the same relative path exactly the same with your development environment. My typical structure is:

project/
    |- config.json
    |- main.go
    |- package1/
    |- package2/
    |- static/
       |- templates/
       |  |- index.html
       |  |- base.html
       |- css/
       |- javascript/
       |- image/

When you are ready to run your application from command line, be sure to copy your config.json and the static/ directory to the same level as your executable binary. Then all you need to do is to use relative paths in your code without any nightmares.

0

For the records: as we just have two templates, we resorted to store them as strings in a go file so that they get compiled. Our html templates are very simple, so this is a reasonable way to do it.

transient_loop
  • 5,984
  • 15
  • 58
  • 117