filego

How to prevent a program B to archive/delete a file that is currently opened by a program A in Go?


Programming language: Go latest version

I am working on a program A that needs to archive log files in a directory, excluding the one that is open by a program B which uses it for logging. The file that is open will be closed after a specific duration, let's say 24 hours, and will then become available for archiving. Both programs run independently, obviously.

The current implementation does not check whether the file is open in program B since I recently discovered it was archiving the opened log file, I assumed it will not do it; Basically it copies the file to the archive and deletes it.

What would be a reliable way to check if a file is currently opened by another Go program before attempting to archive it?

I have already written the archiver, but I won't post it here since there is non relevant code to the question therefore I only add the code that does the archiving and deletion.

I have tried with both functions Open and OpenFile with no success.

func zipFile(filename string) error {
    file, err := os.Open(filename)

    if err != nil {
        return err
    }

    defer func(file *os.File) {
        err = file.Close()

        if err != nil {
            fmt.Println(err)
        }
    }(file)

    fileInfo, err := file.Stat()

    if err != nil {
        return err
    }

    archiveFile, err := os.Create(fmt.Sprintf("%s.zip", filename))

    if err != nil {
        return err
    }

    defer func(archiveFile *os.File) {
        err = archiveFile.Close()

        if err != nil {
            fmt.Println(err)
        }
    }(archiveFile)

    zipWriter := zip.NewWriter(archiveFile)

    if err != nil {
        return err
    }

    defer func(zipWriter *zip.Writer) {
        err = zipWriter.Close()

        if err != nil {
            fmt.Println(err)
        }
    }(zipWriter)

    writer, err := zipWriter.Create(fileInfo.Name())

    if err != nil {
        return err
    }

    _, err = io.Copy(writer, file)

    if err != nil {
        return err
    }

    err = os.Remove(filename)

    if err != nil {
        return err
    }

    fmt.Println(fmt.Sprintf("file %s was zipped", fileInfo.Name()))

    return nil
}

Solution

  • The problem of knowing whether a certain file is open by another process, irrespective of the process, is very dependent on the underlying operating system. As a result, there is no general, cross platform way to answer the question. For *NIX systems (Unix, Linux, macOS), the best way to answer the question is to use the core util lsof ("list open files").

    On macOS, that looks something like this:

    package main
    
    import (
        "fmt"
        "os/exec"
        "strings"
    )
    
    func main() {
        out, _ := exec.Command("lsof").Output()
        for _, line := range strings.Split(string(out), "\n") {
            // Skip anything that's not a regular file
            if !strings.Contains(line, "REG") { 
                continue
            }
    
            fields := strings.Fields(line)
    
            // The macOS lsof command has the filename in the ninth field
            fmt.Println(fields[8])
        }
    }
    

    Alternatively, you could find a lib that abstracts this, like this one: https://github.com/wheelcomplex/lsof

    More on lsof: https://en.wikipedia.org/wiki/Lsof