Why won't calibre read the metadata on a recreated .epub file?

759 Views Asked by At

I am writing a program that takes an .epub file, unzips it, edits the content.opt file to add custom metadata, then zip the contents to create a new .epub file. I am using calibre as both my e-reader and my .epub editor, since calibre makes it very easy to edit both the metadata for an .epub as well as the contents of an .epub file.

I am able to successfully create a new .epub file. I have tested this new file can be read both with calibre and my Kobo e-reader.

However, none of the metadata from the original .epub file transfers over to the new .epub file. Additionally I am unable to edit the .epub file in calibre. When I try I get the error "No META-INF/container.xml in epub". I have tried using multiple .epub files and I get the same results and errors.

Unzipped, the contents of the original .epub file is as follows:

META/INF
  ↳container.xml
content.opf
mimetype
pages_styles.css
[title]_split_000.xhtml
[title]_split_001.xhtml
.....
[title]_split_012.xhtml
[title]_split_013.xhtml
stylesheet.css
toc.ncx

The unzipped directory for the newly created .epub file is identical to the original. Running diff -r -q /[title]_original /[title]_recreated produces no output, which would indicate they are in fact identical. So I am unsure how calibre can read one file and not read another. The error seems to indicate that calibre is somehow unable to find the META-INF/container.xml file, which is used to tell an e-reader where metadata is being stored in the directory.

Note: I am not editing any content for the original .epub during the unzipping or zipping process until I am able to figure out what is happening.

I am running the command go run main.go zip.go in the directory with the two go files and the .epub file [title]:

main.go

package main

import (
// "log"
// "strings"
)

type FileLocations struct {
    src  string
    ext  string
    dest string
}

func main() {

    fileName := "[title]"
    temp := FileLocations{
        src:  fileName,
        ext:  ".epub",
        dest: fileName,
    }

    // Unzip the zip/epub file
    UnzipHelper(temp.src, temp.ext, temp.dest)

    // Zip the modified directory
    ZipHelper(temp.src, temp.ext)
}

func UnzipHelper(src string, ext string, dest string) error {
    _, err := Unzip(src, ext, dest)
    if err != nil {
        return err
    }
    return nil
}

func ZipHelper(src string, ext string) error {
    err := Zip(src, ext)
    if err != nil {
        return err
    }
    return nil
}

zip.go

package main

import (
    "archive/zip"
    "fmt"
    "io"
    "log"
    "os"
    "path/filepath"
    "strings"
)

func Unzip(src string, ext string, dest string) ([]string, error) {

    file := src + ext

    var filenames []string

    r, err := zip.OpenReader(file)
    if err != nil {
        return filenames, err
    }
    defer r.Close()

    for _, f := range r.File {

        // Store filename/path for returning and using later on
        fpath := filepath.Join(dest, f.Name)

        // Check for ZipSlip
        if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
            return filenames, fmt.Errorf("%s: illegal file path", fpath)
        }

        filenames = append(filenames, fpath)

        if f.FileInfo().IsDir() {
            // Make Folder
            os.MkdirAll(fpath, os.ModePerm)
            continue
        }

        // Make File
        if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
            return filenames, err
        }

        outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
        if err != nil {
            return filenames, err
        }

        rc, err := f.Open()
        if err != nil {
            return filenames, err
        }

        _, err = io.Copy(outFile, rc)

        // Close the file without defer to close before next iteration of loop
        outFile.Close()
        rc.Close()

        if err != nil {
            return filenames, err
        }
    }

    // Remove zip file so it can be recreated later
    os.Remove(file)

    return filenames, nil
}

func Zip(filename string, ext string) error {
    // Creates .epub file
    file, err := os.Create(filename + ext)
    if err != nil {
        log.Fatal("os.Create(filename) error: ", err)
    }
    defer file.Close()

    w := zip.NewWriter(file)
    defer w.Close()

    walker := func(path string, info os.FileInfo, err error) error {
        fmt.Println("Crawling: " + path)
        if err != nil {
            return err
        }
        if info.IsDir() {
            return nil
        }
        file, err := os.Open(path)
        if err != nil {
            return err
        }
        defer file.Close()

        f, err := w.Create(path)
        if err != nil {
            return err
        }

        _, err = io.Copy(f, file)
        if err != nil {
            return err
        }

        return nil
    }

    err = filepath.Walk(filename, walker)
    if err != nil {
        log.Fatal("filepath.Walk error: ", err)
    }
    return err
}
2

There are 2 best solutions below

0
On

I'm a month late, but I ran into the same issue and realized I was zipping the folder holding the epub contents, instead of contents inside the folder.

When you zip the folder, the resulting .zip file will follow the same directory structure, so you'll have a subfolder holding the actual ebook contents. The reason you're getting this error is because the META-INF is inside this subfolder instead of at the root.

Example: if you have the contents of your epub in a folder called temp_files, when you zip this folder the file structure will be as follows:

+ your_zipped_file.zip
    + temp_files
        - META-INF
        - OEBPS
        - mimetype

When it should be:

+ your_zipped_file.zip
    -META-INF
    -OEBPS
    -mimetype

(english is not my first language so forgive any mistakes)

0
On

I noticed that container.xml is located in the subfolder, so the issue is most likely caused by incorrect directory processing.
Documentation on zip package states that you can create a directory instead of a file by adding a trailing slash to the name (https://golang.org/pkg/archive/zip/#Writer.Create)
Have you tried this approach?