Add temporary directory helpers and a createDirectories write option - #3045
Conversation
|
Is there a reason why these aren't just implemented in terms of std::filesystem wherever possible? Are those inadequate in a way I have not previously appreciated? |
|
@lgritz I was considering exactly that while putting this together - but MaterialX is used in a whole bunch of different places javascript bindings via emscripten, and I wasn't 100% sure that std::filesystem is supported in all the places. Also hoping to land this fairly easily, as a precursor to some other more important work - so didn't want to rock the boat too much. Given you also flagged what I was thinking I'll file an issue and we can come back to std::filesystem conversion as a separate piece of work. In my very cursory investigation with Claude, it did notice that std::filesystem calls throw exceptions that are more specific that std::exception, but the MaterialX pattern for exceptions is to inherit from that. So I wonder if moving to std::filesystem might invalidate some client code where its catching the |
|
Don't let me derail you. In all honestly, I was thinking "gosh, I just minimally wrap std::filesystem for some of these... are there shortcomings I'm unaware of?" I assume these are holdovers from the pre-C++17 days. |
|
Thanks for this contribution, @ld-kerley! On the Two suggestions from an initial review of the code:
Otherwise this looks good to me, and let's aim to include it in our v1.39.6 release. |
Add three file system methods to FilePath: - getSystemTemporaryDirectory() returns the platform temporary directory, queried with GetTempPath on Windows and the TMPDIR, TMP, TEMP and TEMPDIR environment variables on POSIX, falling back to /tmp. - createTemporaryDirectory() creates a uniquely-named directory under a given parent, defaulting to the system temporary directory, and throws if it cannot be created. POSIX platforms use mkdtemp(), which names and creates the directory atomically and restricts access to the owner. Windows has no equivalent, so candidate names are passed to CreateDirectory, which fails rather than succeeding on an existing path, and only a collision is retried. - removeDirectory() removes a directory, optionally recursively, classifying entries with lstat or the reparse point attribute so that symbolic links are removed without deleting their targets' contents. Add an XmlWriteOptions::createDirectories flag, which causes writeToXmlFile() to create the parent directory hierarchy of the target file if it does not already exist. Tests cover the default and explicit parent directories, the uniqueness and owner-only permissions of each created directory, the reporting of a creation failure, directory removal with and without recursion, and the round-tripping of a document written to a newly created hierarchy.
Throw when writeToXmlFile cannot open its output file Report a failed stream open as ExceptionFileMissing, rather than silently writing nothing, and handle the new exception in the viewer and graph editor save paths. Skip the permission-failure test cases when running as root, which bypasses the permission checks that they rely on.
ebc0332 to
6fd9e52
Compare
|
Thanks for the revision, @ld-kerley! The One remaining request follows from the new throw. Three existing callers of
With those three covered, let's aim to include this in our v1.39.6 release. |
|
Hi, I have a question if MaterialX should own directory operations. In embedded/protected environments, the application has security, user-specific filesystem permissions, threading constraints, and cleanup rules that MaterialX might know about, esp. when the SDK is several layers below the application. Can we instead depend on caller to provide the paths etc? |
|
@ashwinbhat - how are directory operations different from file writing operations - from an embedded security perspective? MaterialXFormat already supports writing files - is that similarly a problem for security - and how are you handling that if thats the case? I think we could scope this functionality more tightly to the OSL Network generator - if that makes it easier to remove from embedded environments that don't support that generator - but I do think this code needs to live on the MaterialX side somewhere. The eventual goal here is to allow the OSL Network generator to be able to on-the-fly code gen new OSL nodes from locally defined custom nodedef elements in the document. |
|
In our usage we don't use any write apis from MaterialX, it's mainly read operations. Anytime we need to write we get the stream and write to filesystem higher up in the stack. |
|
Are there concrete compromises we can make here to make things easier from a security side of things for you - while still moving this forward? would it help if we introduced a If feels like moving this code to be only inside the OSL Network generator would "hide" things - in a way that might make them less easily to audit? Or do you think this path would be more helpful? |
|
Right now it looks like the main consumer of this will be OSL Network generator. I'm ok with isolating this to OSL Network generator instead of MaterialXFormat. We don't need to add ifdef on API headers, as it might create more issues with multiple MaterialX installs etc. |
|
How do you feel about that stylistically @jstone-lucasfilm ? I'm open to any approach that allows us to move forwards here. If thats the route here - then it might make sense to just close this PR - and when I put up the final OSL Network parity PR - I'll include the code here (or some modified version of it) in the OSL Network source files. The one downside to that approach might be - it feels less natural to unit test a generic file system feature if the code is hidden in the network generator - so we would likely just end up with the minimal set of features OSL Network generator needs - not the full interface I'm proposing here. |
|
Thanks for the revision, @ld-kerley! The three callers are now covered, the "Saved translated material" dialog correctly follows the write, and the Graph Editor wrapper is a welcome addition. On the question of where this API should live, my recommendation would be to keep it in
@ashwinbhat, thanks for raising this concern, and I think the substance of it belongs with #2739 rather than with this PR. The API here is opt-in, with the same footprint as our existing write functions for any host that only reads, and the implementation is deliberately conservative. The risk of undeleted temporary folders arises in the on-demand compile path of #2739, which creates a temporary directory without removing it, and I'd suggest we address that in the review of that PR, either by having the generator clean up after itself or by documenting One remaining request follows from my earlier note on With that change, I'd recommend that we include this in v1.39.6. |
(This work is extracted and refined from #2739)
This adds a small set of file system utilities for creating and removing temporary directories, along with an option for
writeToXmlFile()to create the directory hierarchy of its target file. Together these let tools and tests stagegenerated documents in scratch locations without pre-creating the directory structure themselves, which currently requires each caller to reimplement platform-specific temporary directory logic.
The following specific changes are included:
FilePath::getSystemTemporaryDirectory(), returning the platform temporary directory. This is queried withGetTempPathon Windows, and with theTMPDIR,TMP,TEMPandTEMPDIRenvironment variables on POSIX,falling back to
/tmp. The directory is not created, and its existence is not guaranteed.FilePath::createTemporaryDirectory(), creating a uniquely named directory under a given parent and defaulting to the system temporary directory, and throwing anExceptionif the directory cannot be created. POSIX platforms usemkdtemp(), which generates the name, creates the directory atomically, and restricts access to the owner. Windows has no equivalent, so candidate names are passed toCreateDirectory, which fails rather than succeeding when the path already exists; only a name collision is retried, and any other error is reported.FilePath::removeDirectory(), removing a directory and optionally its contents, and returning whether the removal succeeded. A recursive removal classifies entries withlstator the reparse point attribute, so that symbolic links within the directory are removed without deleting the contents of their targets.XmlWriteOptions::createDirectoriesflag, causingwriteToXmlFile()to create the parent directory hierarchy of the target file if it does not already exist. The flag defaults to false, so the behavior of existing code is unchanged.New cases in
MaterialXTestcover the default and explicit parent directories, the uniqueness and owner-only permissions of each created directory, the reporting of a creation failure under a read-only parent, directory removal with and without recursion, the preservation of a symbolic link's target during a recursive removal, and the round-tripping of a document written into a newly created hierarchy. The permission, read-only parent and symbolic link cases are guarded to POSIX. I have run the full test suite on macOS; the Windows code paths are exercised only by CI.