diff --git a/meshroom/nodes/aliceVision/SketchfabUpload.py b/meshroom/nodes/aliceVision/SketchfabUpload.py new file mode 100644 index 00000000..e0444d2e --- /dev/null +++ b/meshroom/nodes/aliceVision/SketchfabUpload.py @@ -0,0 +1,200 @@ +__version__ = "1.0" + +from meshroom.core import desc +import glob +import os +import json +import zipfile +import requests +import io + + +class BufferReader(io.BytesIO): # object to call the callback while the file is being uploaded + def __init__(self, buf=b'', + callback=None, + cb_args=(), + cb_kwargs={}): + self._callback = callback + self._cb_args = cb_args + self._cb_kwargs = cb_kwargs + self._progress = 0 + self._len = len(buf) + io.BytesIO.__init__(self, buf) + + def __len__(self): + return self._len + + def read(self, n=-1): + chunk = io.BytesIO.read(self, n) + self._progress += int(len(chunk)) + self._cb_kwargs.update({ + 'size' : self._len, + 'progress': self._progress + }) + if self._callback: + try: + self._callback(*self._cb_args, **self._cb_kwargs) + except Exception as e: # catches exception from the callback + self._cb_kwargs['logManager'].logger.warning('Error at callback: {}'.format(e)) + return chunk + +def progressUpdate(size=None, progress=None, logManager=None): + if not logManager.progressBar: + logManager.makeProgressBar(size, 'Upload progress:') + + logManager.updateProgressBar(progress) + +class SketchfabUpload(desc.Node): + size = desc.DynamicNodeSize('inputFiles') + inputs = [ + desc.ListAttribute( + elementDesc=desc.File( + name="input", + label="Input", + description="", + value="", + uid=[0], + ), + name="inputFiles", + label="Input Files", + description="Input Files to export.", + group="", + ), + desc.ChoiceParam( + name='maxSize', + label='Maximum Upload Size', + description='The maximum upload size in MB.', + value=50, + values=(50, 200, 500), + exclusive=True, + uid=[0], + advanced=True, + ), + desc.BoolParam( + name='limitSize', + label='Limit Upload Size', + description='Change maximum download size in advanced attributes.', + value=True, + uid=[0], + ), + desc.StringParam( + name='apiToken', + label='API Token', + description='Get your token from https://sketchfab.com/settings/password', + value='', + uid=[0], + ), + desc.StringParam( + name='title', + label='Title', + description='Title cannot be longer than 48 characters.', + value='', + uid=[0], + ), + desc.StringParam( + name='description', + label='Description', + description='Description cannot be longer than 1024 characters.', + value='', + uid=[0], + ), + desc.StringParam( + name='license', + label='License', + description='License label.', + value='CC Attribution', + uid=[0], + ), + desc.BoolParam( + name='isPublished', + label='Publish', + description='If the model is not published it will be saved as a draft.', + value=False, + uid=[0], + ), + desc.BoolParam( + name='isInspectable', + label='Inspectable', + description='Allow 2D view in model inspector.', + value=True, + uid=[0], + ), + desc.ChoiceParam( + name='verboseLevel', + label='Verbose Level', + description='''verbosity level (critical, error, warning, info, debug).''', + value='info', + values=['critical', 'error', 'warning', 'info', 'debug'], + exclusive=True, + uid=[], + ), + ] + + def resolvedPaths(self, inputFiles): + paths = [] + for inputFile in inputFiles: + for f in glob.glob(inputFile.value): + paths.append(f) + return paths + + def upload(self, apiToken, modelFile, data, chunk): + modelEndpoint = 'https://api.sketchfab.com/v3/models' + f = open(modelFile, 'rb') + file = {'modelFile': (os.path.basename(modelFile), f.read()), **data} + (files, contentType) = requests.packages.urllib3.filepost.encode_multipart_formdata(file) + headers = {'Authorization': 'Token {}'.format(apiToken), 'Content-Type': contentType} + body = BufferReader(files, progressUpdate, cb_kwargs={'logManager': chunk.logManager}) + chunk.logger.info('Uploading...') + try: + r = requests.post( + modelEndpoint, **{'data': body, 'headers': headers}) + f.close() + chunk.logManager.completeProgressBar() + except requests.exceptions.RequestException as e: + f.close() + chunk.logger.error(u'An error occured: {}'.format(e)) + raise RuntimeError() + if r.status_code != requests.codes.created: + chunk.logger.error(u'Upload failed with error: {}'.format(r.json())) + raise RuntimeError() + + def processChunk(self, chunk): + chunk.logManager.waitUntilCleared() + chunk.logger.setLevel(chunk.logManager.textToLevel(chunk.node.verboseLevel.value)) + + if not chunk.node.inputFiles: + chunk.logger.warning('Nothing to upload') + return + if len(chunk.node.title.value) > 48: + chunk.logger.error('Title cannot be longer than 48 characters.') + raise RuntimeError() + if len(chunk.node.description.value) > 1024: + chunk.logger.error('Description cannot be longer than 1024 characters.') + raise RuntimeError() + if chunk.node.apiToken.value == '': + chunk.logger.error('Need API token.') + raise RuntimeError() + data = { + 'name': chunk.node.title.value, + 'description': chunk.node.description.value, + 'license': chunk.node.license.value, + 'isPublished': chunk.node.isPublished.value, + 'isInspectable': chunk.node.isInspectable.value + } + # pack files into .zip to reduce file size and simplify process + uploadFile = os.path.join(chunk.node.internalFolder, 'temp.zip') + files = self.resolvedPaths(chunk.node.inputFiles.value) + zf = zipfile.ZipFile(uploadFile, 'w') + for file in files: + zf.write(file, os.path.basename(file)) + zf.close() + chunk.logger.info('Successfully created {}'.format(uploadFile)) + + fileSize = os.path.getsize(uploadFile)/1000000 + chunk.logger.info('File size: {}MB'.format(fileSize)) + if chunk.node.limitSize.value and fileSize > chunk.node.maxSize.value: + chunk.logger.error('File too big.') + raise RuntimeError() + + self.upload(chunk.node.apiToken.value, uploadFile, data, chunk) + chunk.logger.info('Upload successful. Your model is being processed on Sketchfab. It may take some time to show up on your "models" page.') diff --git a/requirements.txt b/requirements.txt index 7daba766..9caca220 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ psutil>=5.6.3 enum34;python_version<"3.4" PySide2==5.13.0 markdown==2.6.11 +requests==2.22.0