masaccio / numbers-parser

Python module for parsing Apple Numbers .numbers files
MIT License
201 stars 14 forks source link

Save document in "Package" mode #76

Closed darioclock closed 3 months ago

darioclock commented 4 months ago

A Numbers file can either be in "Package" mode or "Single File" mode. One can choose the format of the file by going to File -> Advanced -> Change File Type. When the file is in Package mode, the document can be opened and read but it can't be saved.

To Reproduce from numbers_parser import Document doc_path = "./doc_in_package_mode.numbers" doc = Document(doc_path) doc.save(doc_path)

Expected behavior I expect the file to be saved.

masaccio commented 4 months ago

Solution appears to be to to add Metadata/Properties.plist to the new zipfile. Can you test that this works: unzip the newly saved file, create Metadata/Properties.plist using the following content:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>fileFormatVersion</key>
    <string>13.2.1</string>
    <false/>
</dict>
</plist>

And then re-zip. With an empty document this worked for me. If this works in your example then it's easy enough to re-generate the missing file on save. I'm also wondering whether document images are preserved?

darioclock commented 4 months ago

Thanks for the quick reply!

My file is in zip format and already contains the Metadata/Properties.plist Here is what's inside:

// !!! BINARY PROPERTY LIST WARNING !!!
//
// The pretty-printed property list below has been created
// from a binary version on disk and should not be saved as
// the ASCII format is a subset of the binary representation!
//
{   documentUUID = "somenumbers";
    fileFormatVersion = "13.2.1";
    isMultiPage = :false;
    privateUUID = "somenumbers";
    revision = "somenumbers";
    shareUUID = "somenumbers";
    stableDocumentUUID = "somenumbers";
    versionUUID = "somenumbers";
}

which seems pretty different from what you are suggesting to replace it with.

This is the traceback from the issue:

Traceback (most recent call last):
  File "/Users/.../test.py", line 153, in <module>
    source_doc.save(source_path)
  File "/Users/.../.pyenv/versions/3.9.6/lib/python3.9/site-packages/numbers_parser/document.py", line 143, in save
    write_numbers_file(filename, self._model.file_store)
  File "/Users/.../.pyenv/versions/3.9.6/lib/python3.9/site-packages/numbers_parser/file.py", line 60, in write_numbers_file
    zipf = ZipFile(filename, "w")
  File "/Users/.../.pyenv/versions/3.9.6/lib/python3.9/zipfile.py", line 1239, in __init__
    self.fp = io.open(file, filemode)
IsADirectoryError: [Errno 21] Is a directory: '/Users/.../myfileinzipformat.numbers'

It's failing the save because the file.numbers is now considered a directory when in package format. It's weird that opening and loading the document works without issue but the saving fails.

It’s weird cause it seems ZipFile seems to accept a path as an input. I’ll look at their code next if it’s open.

masaccio commented 4 months ago

I believe this is fixed in v4.9.0.

darioclock commented 4 months ago

Mmh, I get the same error in 4.9.1. With same traceback (sorry I closed the issue by mistake).

masaccio commented 4 months ago

The same traceback? I installed into a new environment and I can't reproduce the exception. I do get a corrupted Numbers file though (fails to open).

masaccio commented 4 months ago

v4.9.3 includes fixed support.

darioclock commented 4 months ago

I'm really sorry to be the bearer of bad news. Even with 4.9.3 I still fails at the same step. I tired with Python 3.9.6 and 3.10.5.

The creation of the ZipFile class in write_numbers_file fails because the path that I provide /my/path/to/file.numbers is identified as a directory when the file is in "Package" mode.

Please keep in mind that in my example I'm opening an already existing document that already is in "Package" mode and then I'm modifying it and intending to save it with the same name.

masaccio commented 4 months ago

Oh you're trying to overwrite the folder? That won't work -- you will need to write to a different location. I don't plan to support writing the package format.

darioclock commented 4 months ago

Understood, thanks for the support.

For completion, here is the reason why I need to overwrite the document and can't create a new one.

I tried using other sharing strategies like "Box" that allow to keep the numbers file in a "Single File" mode but I had multiple bugs/issues with the UI. Doesn't work great for live collaboration. The only solution that I see at this point is to use AppleScript to do the mods that I need.

masaccio commented 4 months ago

I see. It's a more significant change to support package mode for writes. It needs some thought about how to convert single-file to package versus overwriting existing packages. I'll take a look but it'll not be this weekend.

darioclock commented 4 months ago

Thank you very much for the support.

masaccio commented 4 months ago

In v4.10.0 Document.save() now takes an additional package argument that will write a folder-based package. It will refuse to do so if the target is a file or if the target looks malformed.

The file IO has been extensively rewritten but tests to full coverage and I've added more exception checking and sanity checking, so things should be sound. I'd certainly keep copies of documents though in case something bad happens when overwriting. I keep my checked out git repos in iCloud and occasionally Apple's synchronisation throws a bit of a wobbly.

darioclock commented 4 months ago

Thank you very much! I'll give it a try as soon as possible, but it might not be this weekend.

darioclock commented 4 months ago

Hi!

I gave it a quick try and it seems it works as expected! It seems it also maintains the "Sharing" features when sharing with iCloud but I wanna check-in with my colleagues and see if everything is ok on their end too.

I noticed that I had to run my script twice for the mods to appear. The first time the mods didn't take place at all. Not sure why that was the case but it could be because of iCloud. I will test more thoroughly as I have more chances to run it.

I'd wait to test more before closing the issue.

Thank you again!

darioclock commented 3 months ago

Hi,

I have tried this feature for the past week and it seems to work great!

With this I can programmatically edit a file that is shared in iCloud without affecting the access that other people already have on the file.