"operation not permitted" when writing a file

nraynaud
Enthusiast

"operation not permitted" when writing a file

nraynaud
Enthusiast
Enthusiast

Hi all, 

 

I am trying to create a simple add-in in python, simply to save a body to an STL file. I am a complete noob at the Fusion 360 API, and the learning curve is steep, it always crashed without any meaning error message!

 

I was about to get things going, when I got this error message when saving the file: "PermissionError: Operation not permitted".

I am suspecting some Mac OS process sandboxing issue, but it has to be noted that I can save an STL file manually from Fusion 360 in the same directory, so it's not a complete interdiction.

 

here is the message: 

Capture d’écran 2017-09-17 à 10.16.11.png

 

here is the work in progress: 

 

import json

import adsk.cam
import adsk.core
import adsk.fusion
import traceback

USER_PARAM = 'exporter_stl'

keepHandlers = []
deletableObjects = []


def find_brep(chain, component, ui):
    if len(chain) > 1:
        if component.objectType == 'adsk::fusion::Component':
            child = component.occurrences.itemByName(chain[0])
        else:
            child = component.childOccurrences.itemByName(chain[0])
        return find_brep(chain[1:], child, ui)
    elif len(chain) == 1:
        return component.bRepBodies.itemByName(chain[0])
    return None


class ExportCommandCreatedEventHandler(adsk.core.CommandCreatedEventHandler):
    def notify(self, args):
        cmd = args.command
        app = adsk.core.Application.get()
        ui = app.userInterface
        try:
            design = app.activeProduct
            dir_param = design.userParameters.itemByName(USER_PARAM)
            data = None
            if dir_param:
                data = json.loads(dir_param.comment)
            else:
                dialog = ui.createFileDialog()
                dialog.title = 'Select export file'
                dialog.filter = '*.*'
                result = dialog.showSave()
                if result == adsk.core.DialogResults.DialogOK:
                    file_name = dialog.filename
                    ui.messageBox('select body')
                    selected = ui.selectEntity('select body', 'Bodies,MeshBodies')
                    entity = selected.entity
                    chain = [entity.name]
                    obj = entity
                    while True:
                        obj = obj.assemblyContext
                        if obj:
                            chain.append(obj.name)
                        else:
                            break
                    data = {'file': file_name, 'chain': list(reversed(chain))}
                    json_params = json.dumps(data)
                    design.userParameters.add(USER_PARAM, adsk.core.ValueInput.createByString('0'), '', json_params)
            if data:
                body = find_brep(data['chain'], design.rootComponent, ui)
                if body:
                    ui.messageBox('body found: ' + body.name)
                    exportMgr = design.exportManager
                    ui.messageBox('file: ' + data['file'])
                    with open(data['file'] + '.txt', 'w') as out_file:
                        out_file.write('whatever')
                    stl_export_options = exportMgr.createSTLExportOptions(body)
                    stl_export_options.sendToPrintUtility = False
                    stl_export_options.meshRefinement = 0
                    stl_export_options.filename = data['file']
                    exportMgr.execute(stl_export_options)
                else:
                    ui.messageBox('.'.join(data['chain']) + 'not found')
        except:
            ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))


def run(context):
    app = adsk.core.Application.get()
    ui = app.userInterface
    try:
        print('STARTING')
        cmdDefs = ui.commandDefinitions
        myButton = ui.commandDefinitions.itemById('ExportDesign')
        if myButton:
            myButton.deleteMe()
        myButton = cmdDefs.addButtonDefinition('ExportDesign', 'Export STL', 'Export the design in various formats.')
        onCommandCreated = ExportCommandCreatedEventHandler()
        myButton.commandCreated.add(onCommandCreated)
        keepHandlers.append(onCommandCreated)
        ui.allToolbarPanels.itemById('SolidMakePanel').controls.addCommand(myButton, '', False)
    except:
        if ui:
            ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))


def stop(context):
    ui = None
    try:
        app = adsk.core.Application.get()
        ui = app.userInterface
        button = ui.commandDefinitions.itemById('ExportDesign')
        if button:
            button.deleteMe()
        global keepHandlers
        keepHandlers = []
        # for obj in deletableObjects:
        #    obj.deleteMe()
    except:
        if ui:
            ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))

 

thanks for your help.

0 Likes
Reply
1,098 Views
5 Replies
Replies (5)

marshaltu
Autodesk
Autodesk

Hello,

 

It was a Mac OS process sandboxing issue as you guessed. In Fusion 360, you didn't see the issue when you save STL manually was because the permission will be retrieved automatically when users select the folder by dialog. However in your codes, the "file" path was sometimes gotten by a JSON data, not by dialog. 

 

Thanks,

Marshal



Marshal Tu
Fusion Developer
>
0 Likes

nraynaud
Enthusiast
Enthusiast

Thanks, is there a way for me to keep the access permission? Some kind of cookie?

0 Likes

marshaltu
Autodesk
Autodesk

Hello,

 

You can get the permission programmatically by "Security-scoped bookmarks" described in the following link. Meanwhile you may have to use some Python library like this to call the api from Python. Unfortunately I never try that in Python. Have a good luck. 

 

https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso?...

 

Thanks,

Marshal



Marshal Tu
Fusion Developer
>
0 Likes

nraynaud
Enthusiast
Enthusiast

Thanks a lot.

I will look into that.

I couldn't find the API at all with google.

 

Nico

0 Likes

nraynaud
Enthusiast
Enthusiast

Thank you,

I could get something working, I'm posting where I am at, so that google picks it up for the others coming after me.

 

I installed pyobjc and setuptools in the "libs" dir.

since I used brew I encountered a bug that I countered by creating the file "setup.cfg" whose content is :

[install]
prefix= 

 

pip3.5 install -t libs setuptools
pip3.5 install -t libs pyobjc

 

my addin directory looks like that now (Export is the name of my addin): 

$ ls
Export.manifest	Export.py	libs		setup.cfg

 

 

here is the state of my code it's far from finished, but I think it's good enough for people googling the issue.

 

 

import os
import sys

sys.path.append(os.path.join(os.path.dirname(__file__), 'libs'))

import Foundation
import tempfile
from shutil import copyfile

import json

import adsk.cam
import adsk.core
import adsk.fusion
import traceback

USER_PARAM = 'exporter_stl'

keepHandlers = []
deletableObjects = []


def find_brep(chain, component, ui):
    if len(chain) > 1:
        if component.objectType == 'adsk::fusion::Component':
            child = component.occurrences.itemByName(chain[0])
        else:
            child = component.childOccurrences.itemByName(chain[0])
        return find_brep(chain[1:], child, ui)
    elif len(chain) == 1:
        return component.bRepBodies.itemByName(chain[0])
    return None


class ExportCommandCreatedEventHandler(adsk.core.CommandCreatedEventHandler):
    def notify(self, cmd):
        app = adsk.core.Application.get()
        ui = app.userInterface
        try:
            design = app.activeProduct
            dir_param = design.userParameters.itemByName(USER_PARAM)
            data = None
            if dir_param:
                data = json.loads(dir_param.comment)
            else:
                dialog = ui.createFileDialog()
                dialog.title = 'Select export file'
                dialog.filter = '*.*'
                accessible = dialog.showSave()
                if accessible == adsk.core.DialogResults.DialogOK:
                    file_name = dialog.filename
                    # we can take a URL only if the file exists, so we are doing a "touch"
                    with open(file_name, 'a'):
                        os.utime(file_name)
                    url = Foundation.NSURL.alloc().initFileURLWithPath_(file_name)
                    bookmark, error = url.bookmarkDataWithOptions_includingResourceValuesForKeys_relativeToURL_error_(
                        Foundation.NSURLBookmarkCreationWithSecurityScope,
                        None,
                        None,
                        None)
                    sys.stdout.flush()
                    # base64 encode the bookmark so it can be jsonified
                    the_bytes = bookmark.base64EncodedStringWithOptions_(0)
                    ui.messageBox('select body')
                    selected = ui.selectEntity('select body', 'Bodies,MeshBodies')
                    entity = selected.entity
                    chain = [entity.name]
                    obj = entity
                    while True:
                        obj = obj.assemblyContext
                        if obj:
                            chain.append(obj.name)
                        else:
                            break

                    data = {'file': the_bytes, 'chain': list(reversed(chain))}
                    json_params = json.dumps(data)
                    design.userParameters.add(USER_PARAM, adsk.core.ValueInput.createByString('0'), '', json_params)
            if data:
                body = find_brep(data['chain'], design.rootComponent, ui)
                if body:
                    nsdata = Foundation.NSData.alloc().initWithBase64EncodedString_options_(data['file'], 0)
                    url, is_stale, error = Foundation.NSURL.URLByResolvingBookmarkData_options_relativeToURL_bookmarkDataIsStale_error_(
                        nsdata,
                        Foundation.NSURLBookmarkResolutionWithSecurityScope,
                        None,
                        None,
                        None)
                    file_path = url.path()
                    accessible = url.startAccessingSecurityScopedResource()
                    if accessible:
                        try:
                            # using the temp dir, for some reasons exportManager tries to access the directory
                            # surrounding the output file
                            with tempfile.NamedTemporaryFile(suffix='.stl') as temp_file:
                                export_manager = design.exportManager
                                options = export_manager.createSTLExportOptions(body, temp_file.name)
                                options.sendToPrintUtility = False
                                options.meshRefinement = 0
                                export_manager.execute(options)
                                temp_file.seek(0)
                                copyfile(temp_file.name, file_path)
                            ui.messageBox('wrote STL file')
                        finally:
                            url.stopAccessingSecurityScopedResource()
                    else:
                        ui.messageBox('file ' + str(url) + ' not accessible!')
                else:
                    ui.messageBox('.'.join(data['chain']) + 'not found')
        except:
            ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))


def run(context):
    app = adsk.core.Application.get()
    ui = app.userInterface
    try:
        print('STARTING')
        cmd_defs = ui.commandDefinitions
        my_button = ui.commandDefinitions.itemById('ExportDesign')
        if my_button:
            my_button.deleteMe()
        my_button = cmd_defs.addButtonDefinition('ExportDesign', 'Export STL', 'Export the design in various formats.')
        on_command_created = ExportCommandCreatedEventHandler()
        my_button.commandCreated.add(on_command_created)
        keepHandlers.append(on_command_created)
        controls = ui.allToolbarPanels.itemById('SolidMakePanel').controls
        previous_control = controls.itemById('ExportDesign')
        if previous_control:
            previous_control.deleteMe()
        controls.addCommand(my_button, '', False)
    except:
        if ui:
            ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))


def stop(context):
    ui = None
    try:
        app = adsk.core.Application.get()
        ui = app.userInterface
        button = ui.commandDefinitions.itemById('ExportDesign')
        if button:
            button.deleteMe()
        global keepHandlers
        keepHandlers = []
        # for obj in deletableObjects:
        #    obj.deleteMe()
    except:
        if ui:
            ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))

 

Thanks for pointing me in the right direction.

 

Nicolas.

 

1 Like