How to circumvent "getFolder" for "doc.saveAs.jpg()"

I have cobbled together the following function, which saves the active document into a picked folder as a JPG. Can I get a reference to the folder with a path string, without calling the getFolder function, which opens a browser window?

async function targetFunction(doc, sanitizedName) {
    try {
        const saveFolder = await fs.getFolder() ;
        console.log(saveFolder);
        const file = await saveFolder.createFile( sanitizedName, {overwrite: true}) ;
        await doc.saveAs.jpg(
          file,
          {
            quality: 7,
          },
          true
        ) ;
    } catch(e) { 
        console.log(e);
    }
}

await executeAsModal(() => targetFunction(doc, sanitizedName));

The logged object looks like this in the console:
screenshot_folder_object_promise

This is not a session token, neither a string, nor a list…
How can I obtain such an object by code? Or is there another, better way, to achieve the same result?
Any help would be greatly appreciated!

You could record batchPlay for this from latest Alchemist version and borrow tokenify() helper function to make a token from file path.

Thanks for the reply! Unfortunately that was the first thing I tried…

async function tokenify(url){
   return fs.createSessionToken(await fs.getEntryWithUrl(url));
}

async function targetFunction(folderPath, doc, sanitizedName) {
    try {
        //const saveFolder = await fs.getFolder() ;
        const saveFolder = tokenify(folderPath) ;
        console.log(saveFolder);
        const file = await saveFolder.createFile( sanitizedName, {overwrite: true}) ;
        await doc.saveAs.jpg(
          file,
          {
            quality: 7,
          },
          true
        ) ;
    } catch(e) { 
        console.log(e);
    }
}

await executeAsModal(() => targetFunction(folderPath, doc, sanitizedName));

Throws this:
screenshotjavascript_error_saveAs01

The logged object after trying to parse the folder is a totally different and createFile doesnt work on it…

You are using it wrong. Token is text string as shown in console. You should paste token directly into function as argument. So you would tokenify the file path. Not the folder path. If that function can’t accept token but instance of File instead… you would need additional conversion to File instance.

I am sorry, Jarda, your responses are a bit cryptic for someone not as well versed as you. I have no issue if you break it down for complete idiots, if you have time…

async function targetFunction(folderPath, doc, sanitizedName) {
    try {
        //const saveFolder = await fs.getFolder() ;
        const saveFile = tokenify(folderPath) ;
        console.log(saveFile);
        const file = await saveFile.createFile( sanitizedName, {overwrite: true}) ;
        await doc.saveAs.jpg(
          file,
          {
            quality: 7,
          },
          true
        ) ;
    } catch(e) { 
        console.log(e);
    }
}

const filePath = exporterPath + "\\936x676\\" + strippedFileName + ".jpg";
await executeAsModal(() => targetFunction(filePath, doc, sanitizedName));

I also tried to use Alchemist to capture the batchPlay function itself, but strangely it outputs two batchPlay commands, one witih an option “fileSave: begin” and one with “fileSave: end” (or something along these lines. ) I have tried 4 or 5 different solutions for this and all of them did not work in one way or another… This is not an issue of “I havent gotten this to work in 10 minutes and am posting now”. It has taken hours until I finally mustered the courage to again post some of my menial questions. I just find UXP very unintuitive and built by professional programmers for professional programmers. Please, my brain is exploding…

I think there’s a few things going on here. You’re passing a full file path to your target function, that (according to the function signature) is supposed to get the path to a folder (not a file). You then create a token of it, but the token is just an identifier, it’s not a file or folder object. Then the function createFile only exists on a folder object. Besides, there’s no method allowing you to create an arbitrary location including previously non-existent subfolders in one go.

I would suggest the following: 1) Assuming you have an existing export folder, you can pass the entry of that folder (if you have only a path you can get an entry via getEntryWithURL, described here). 2) On that folder object you can now call createFile, with the file name (not full path). That function returns an entry to that newly created file, which you can then pass to doc.saveAs.

P.S. You may want to brush up on the docs for accessing and handling files. There are a few concepts that are important to understand and distinguish between:

  • A path to a folder or file (or more generally a URI), is just a string that identifies its location on disk.
  • An ‘entry’ for a file or folder is a proper Javascript object representing a location, with methods to get info about the location or do something with it, e.g. for a folder you can create another folder inside of it or create a file inside of the folder. For an entry you can get a token, but generally it’s a good idea to pass around entries in your code and turn entries into tokens if you need them.
  • A token is (similar to a path) a way to identify a location, but in a somewhat encrypyed way, that informs UXP that you have permission to access that location (or previously gained access). UXP operates on either entries, or tokens. Most of the time batchPlay code requires a token (especially whenever you’re supposed to pass a ‘path’, which is a bit confusing), whereas API functions often require entries. Again you can easily get a token for an entry via createSessionToken, the other way around on the other hand is not easy.

Important: Read API docs carefully to know what a function requires, whether it’s a URI (path), entry or token!

UXP is heavily sandboxed for security reasons. So you don’t get random access to any location on disk. You can get access to a folder or file via:

  • Using getFolder, which brings up a dialog. Importantly, once you have that folder, you have access to any child folder or file or any folder/file you create in there. So in your case it might be acceptable for a user to select a main export folder once and then you operate within that folder subsequently (e.g. create files or subfolders in there).
  • Using getEntryWithURL, specifying a full path to a file or folder. Like you brought up in your own post from recently. This will give you an entry to that folder. This type of random file access however needs permissions to be set in the manifest, as described in this post from a while ago. Note: Obviously you can’t get an entry to a location that does not (or not yet) exist on disk.

In your particular example I would start with getting an entry to your main export location. Something like this:

// Ask the user once:
const mainExportFolder = await fs.getFolder();

// Alternatively get random file access (needs manifest setting):
//const lfs = require("uxp").storage.localFileSystem;
//const mainExportFolder = await lfs.getEntryWithUrl(YOUR_FOLDER_PATH);

Then create your subfolders, no need to ask for permissions again. (You may need some logic to handle the case where the subfolder already exists from a previous export, etc. but that’s up to you). Something like this:

const jpgFolder = await mainExportFolder.createFolder("936x676");

Now you can ask the subfolder to create an entry for the JPG file in the subfolder you just created. Important: this is the file name (with extension), not the full path to it. Something like this:

const jpgFile = await jpgFolder.createFile(fileName, {overwrite: true});

// Save the document, providing file entry:
await doc.saveAs.jpg(jpgFile, ... )
1 Like

Took me a while to read through everything and trying to understand it.
Thank you very much for your precise and indepth analysis, i owe you a beer!

First I didnt actually pass a folder path into getFolder()
Then I confused sessionTokens with folder objects…

In hindsight, I am sorry for such a dumb question!
I just didnt didnt properly get what the documentation was implying.