├── django2_resumable ├── __init__.py ├── tests.py ├── urls.py ├── forms.py ├── views.py ├── widgets.py ├── fields.py ├── templates │ └── django2_resumable │ │ └── file_input.html ├── files.py └── static │ └── django2_resumable │ └── js │ └── resumable.js ├── setup.cfg ├── LICENSE ├── setup.py ├── .gitignore └── README.md /django2_resumable/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [metadata] 5 | license_file = LICENSE -------------------------------------------------------------------------------- /django2_resumable/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /django2_resumable/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | urlpatterns = [ 6 | path('', views.resumable_upload, name='resumable-upload'), 7 | ] 8 | -------------------------------------------------------------------------------- /django2_resumable/forms.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ValidationError 2 | from django.forms import FileField 3 | 4 | from django2_resumable.widgets import ResumableWidget 5 | 6 | 7 | class FormResumableFileField(FileField): 8 | widget = ResumableWidget 9 | 10 | def to_python(self, data): 11 | if self.required: 12 | if not data or data == "None": 13 | raise ValidationError(self.error_messages['empty']) 14 | return data 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Valerio Maggio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /django2_resumable/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from .files import ResumableFile, get_storage, get_chunks_upload_to 3 | 4 | 5 | def resumable_upload(request): 6 | upload_to = get_chunks_upload_to(request) 7 | storage = get_storage(upload_to) 8 | if request.method == 'POST': 9 | chunk = request.FILES.get('file') 10 | r = ResumableFile(storage, request.POST) 11 | if not r.chunk_exists: 12 | r.process_chunk(chunk) 13 | if r.is_complete: 14 | actual_filename = storage.save(r.filename, r.file) 15 | r.delete_chunks() 16 | return HttpResponse(storage.url(actual_filename), status=201) 17 | return HttpResponse('chunk uploaded') 18 | elif request.method == 'GET': 19 | r = ResumableFile(storage, request.GET) 20 | if not r.chunk_exists: 21 | return HttpResponse('chunk not found', status=404) 22 | if r.is_complete: 23 | actual_filename = storage.save(r.filename, r.file) 24 | r.delete_chunks() 25 | return HttpResponse(storage.url(actual_filename), status=201) 26 | return HttpResponse('chunk exists', status=200) 27 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from setuptools import setup 3 | 4 | with open("README.md", "r") as fh: 5 | long_description = fh.read() 6 | 7 | setup( 8 | name='django2-resumable', 9 | version='0.1.1', 10 | author=u'Valerio Maggio', 11 | author_email='valeriomaggio@gmail.com', 12 | packages=['django2_resumable'], 13 | include_package_data=True, 14 | package_data={ 15 | 'django2_resumable': [ 16 | 'templates/django2_resumable/file_input.html', 17 | 'static/django2_resumable/js/resumable.js', 18 | ] 19 | }, 20 | url='https://github.com/leriomaggio/django2-resumable', 21 | license='MIT licence', 22 | description='Django 2.x resumable uploads', 23 | long_description=long_description, 24 | long_description_content_type="text/markdown", 25 | install_requires=[ 26 | 'Django >= 2.0', 27 | ], 28 | classifiers=[ 29 | 'Development Status :: 5 - Production/Stable', 30 | 'Environment :: Web Environment', 31 | 'Framework :: Django', 32 | 'Intended Audience :: Developers', 33 | 'License :: OSI Approved :: MIT License', 34 | 'Natural Language :: English', 35 | 'Programming Language :: Python', 36 | 'Programming Language :: Python :: 3.7', 37 | ], 38 | zip_safe=False, 39 | ) 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # PyCharm Project folder 7 | .idea/ 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # Environments 88 | .env 89 | .venv 90 | env/ 91 | venv/ 92 | ENV/ 93 | env.bak/ 94 | venv.bak/ 95 | 96 | # Spyder project settings 97 | .spyderproject 98 | .spyproject 99 | 100 | # Rope project settings 101 | .ropeproject 102 | 103 | # mkdocs documentation 104 | /site 105 | 106 | # mypy 107 | .mypy_cache/ 108 | -------------------------------------------------------------------------------- /django2_resumable/widgets.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urljoin 2 | 3 | from django.conf import settings 4 | from django.forms import FileInput, CheckboxInput, forms 5 | from django.template import loader 6 | from django.templatetags.static import static 7 | from django.utils.safestring import mark_safe 8 | from django.utils.translation import ugettext_lazy 9 | 10 | 11 | class ResumableWidget(FileInput): 12 | template_name = 'django2_resumable/file_input.html' 13 | clear_checkbox_label = ugettext_lazy('Clear') 14 | 15 | def render(self, name, value, attrs=None, **kwargs): 16 | 17 | if not value: 18 | file_url = '' 19 | else: 20 | if hasattr(value, 'name'): 21 | file_name = value.name 22 | else: 23 | file_name = value 24 | file_url = urljoin(settings.MEDIA_URL, file_name) 25 | 26 | chunkSize = getattr(settings, 'RESUMABLE_CHUNKSIZE', "1*1024*1024") 27 | show_thumb = getattr(settings, 'RESUMABLE_SHOW_THUMB', False) 28 | context = {'name': name, 29 | 'value': value, 30 | 'id': attrs['id'], 31 | 'chunkSize': chunkSize, 32 | 'show_thumb': show_thumb, 33 | 'field_name': self.attrs['field_name'], 34 | 'content_type_id': self.attrs['content_type_id'], 35 | 'file_url': file_url} 36 | 37 | if not self.is_required: 38 | template_with_clear = '%(clear)s ' \ 39 | '' \ 40 | '
' 41 | substitutions = {} 42 | substitutions['clear_checkbox_id'] = attrs['id'] + "-clear-id" 43 | substitutions['clear_checkbox_name'] = attrs['id'] + "-clear" 44 | substitutions['clear_checkbox_label'] = self.clear_checkbox_label 45 | substitutions['clear'] = CheckboxInput().render( 46 | substitutions['clear_checkbox_name'], 47 | False, 48 | attrs={'id': substitutions['clear_checkbox_id']} 49 | ) 50 | clear_checkbox = mark_safe(template_with_clear % substitutions) 51 | context.update({'clear_checkbox': clear_checkbox}) 52 | return loader.render_to_string(self.template_name, context) 53 | 54 | def value_from_datadict(self, data, files, name): 55 | if not self.is_required and data.get("id_" + name + "-clear"): 56 | return False # False signals to clear any existing value, as opposed to just None 57 | if data.get(name, None) in ['None', 'False']: 58 | return None 59 | return data.get(name, None) 60 | 61 | @property 62 | def media(self): 63 | js = ["resumable.js"] 64 | return forms.Media(js=[static("django2_resumable/js/%s" % path) for path in js]) 65 | -------------------------------------------------------------------------------- /django2_resumable/fields.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Field, FileField 2 | from django.core.files.move import file_move_safe 3 | from django.conf import settings 4 | from django.contrib.contenttypes.models import ContentType 5 | 6 | from os import path, makedirs 7 | import urllib 8 | from .forms import FormResumableFileField 9 | from .widgets import ResumableWidget 10 | 11 | 12 | class ResumableFileField(FileField): 13 | def __init__(self, verbose_name=None, name=None, upload_to='', 14 | chunks_upload_to='', **kwargs): 15 | self.chunks_upload_to = chunks_upload_to 16 | super(ResumableFileField, self).__init__(verbose_name, name, upload_to, 17 | **kwargs) 18 | 19 | def pre_save(self, model_instance, add): 20 | 21 | if not self.upload_to or (not callable( 22 | self.upload_to) and self.upload_to == self.chunks_upload_to): 23 | # this condition is verified whether "upload_to" has not been set in the 24 | # definition of field, or it has been set to the same location of the 25 | # chunks folder. 26 | # In those cases, we save some (useless) I/O operations 27 | # (i.e. deleting, and re-creating the same file twice), and 28 | # so the default FileField behaviour will be used/returned. 29 | return super(ResumableFileField, self).pre_save(model_instance, add) 30 | 31 | # if here, upload_to has been set to a different location 32 | # from the chunks_upload_to 33 | file = Field.pre_save(self, model_instance, add) 34 | if file and (not file._committed or self.chunks_upload_to in file.name): 35 | # Commit the file to storage prior to saving the model 36 | fpath = urllib.parse.unquote_plus(file.name.replace(settings.MEDIA_URL, self._safe_media_root())) 37 | basename = path.basename(fpath) 38 | name = self.generate_filename(model_instance, basename) 39 | new_fpath = file.storage.get_available_name( 40 | path.join(self.storage.location, name), 41 | max_length=self.max_length) 42 | basefolder = path.dirname(new_fpath) 43 | if not file.storage.exists(basefolder): 44 | makedirs(basefolder) 45 | file_move_safe(fpath, new_fpath) 46 | # update name 47 | new_basename = path.basename(new_fpath) 48 | new_name = self.generate_filename(model_instance, new_basename) 49 | setattr(model_instance, self.name, new_name) 50 | file._committed = True 51 | file.name = new_name 52 | return file 53 | 54 | def _safe_media_root(self): 55 | if not settings.MEDIA_ROOT.endswith(path.sep): 56 | media_root = settings.MEDIA_ROOT + path.sep 57 | else: 58 | media_root = settings.MEDIA_ROOT 59 | return media_root 60 | 61 | def formfield(self, **kwargs): 62 | content_type_id = ContentType.objects.get_for_model(self.model).id 63 | defaults = { 64 | 'form_class': FormResumableFileField, 65 | 'widget': ResumableWidget(attrs={ 66 | 'content_type_id': content_type_id, 67 | 'field_name': self.name}) 68 | } 69 | kwargs.update(defaults) 70 | return super(ResumableFileField, self).formfield(**kwargs) 71 | -------------------------------------------------------------------------------- /django2_resumable/templates/django2_resumable/file_input.html: -------------------------------------------------------------------------------- 1 | 89 |
90 |

91 | {% if value %} 92 | Currently: 93 | {% if file_url %} 94 | {{ file_url }} 95 | {% if show_thumb %} 96 | File Preview Thumbnail 97 | {% endif %} 98 | {% else %} 99 | {{ value }} 100 | {% endif %} 101 | {{ clear_checkbox }} 102 |
103 | Change: 104 | {% endif %} 105 | 106 | 107 |
108 | 109 |

110 | 111 |
112 | 113 | 114 | -------------------------------------------------------------------------------- /django2_resumable/files.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | from urllib.parse import urljoin 4 | 5 | from django.conf import settings 6 | from django.contrib.contenttypes.models import ContentType 7 | from django.core.files.storage import get_storage_class 8 | 9 | 10 | class ResumableFile: 11 | 12 | def __init__(self, storage, kwargs): 13 | self.storage = storage 14 | self.kwargs = kwargs 15 | self.chunk_suffix = "_part_" 16 | 17 | @property 18 | def chunk_exists(self): 19 | """Checks if the requested chunk exists. 20 | """ 21 | return self.storage.exists(self.current_chunk_name) and \ 22 | self.storage.size(self.current_chunk_name) == int( 23 | self.kwargs.get('resumableCurrentChunkSize')) 24 | 25 | @property 26 | def chunk_names(self): 27 | """Iterates over all stored chunks. 28 | """ 29 | chunks = [] 30 | files = sorted(self.storage.listdir('')[1]) 31 | for f in files: 32 | if f.startswith('{}{}'.format( 33 | self.filename, self.chunk_suffix)): 34 | chunks.append(f) 35 | return chunks 36 | 37 | @property 38 | def current_chunk_name(self): 39 | return "%s%s%s" % ( 40 | self.filename, 41 | self.chunk_suffix, 42 | self.kwargs.get('resumableChunkNumber').zfill(4) 43 | ) 44 | 45 | def chunks(self): 46 | """Iterates over all stored chunks. 47 | """ 48 | files = sorted(self.storage.listdir('')[1]) 49 | for f in files: 50 | if f.startswith('{}{}'.format( 51 | self.filename, self.chunk_suffix)): 52 | yield self.storage.open(f, 'rb').read() 53 | 54 | def delete_chunks(self): 55 | [self.storage.delete(chunk) for chunk in self.chunk_names] 56 | 57 | @property 58 | def file(self): 59 | """Gets the complete file. 60 | """ 61 | if not self.is_complete: 62 | raise Exception('Chunk(s) still missing') 63 | return self 64 | 65 | @property 66 | def filename(self): 67 | """Gets the filename.""" 68 | filename = self.kwargs.get('resumableFilename') 69 | if '/' in filename: 70 | raise Exception('Invalid filename') 71 | return "%s_%s" % ( 72 | self.kwargs.get('resumableTotalSize'), 73 | filename 74 | ) 75 | 76 | @property 77 | def is_complete(self): 78 | """Checks if all chunks are already stored. 79 | """ 80 | print("resumableTotalSize", int(self.kwargs.get('resumableTotalSize')), 81 | ": size", self.size) 82 | return int(self.kwargs.get('resumableTotalSize')) == self.size 83 | 84 | def process_chunk(self, file): 85 | if self.storage.exists(self.current_chunk_name): 86 | self.storage.delete(self.current_chunk_name) 87 | self.storage.save(self.current_chunk_name, file) 88 | 89 | @property 90 | def size(self): 91 | """Gets chunks size. 92 | """ 93 | size = 0 94 | for chunk in self.chunk_names: 95 | size += self.storage.size(chunk) 96 | return size 97 | 98 | 99 | def ensure_dir(f): 100 | d = os.path.dirname(f) 101 | os.makedirs(d, exist_ok=True) 102 | 103 | 104 | def get_chunks_subdir(): 105 | return getattr(settings, 'RESUMABLE_SUBDIR', 'resumable_chunks/') 106 | 107 | 108 | def get_storage(chunks_upload_to): 109 | """ 110 | Looks at the ADMIN_RESUMABLE_STORAGE setting and returns 111 | an instance of the storage class specified. 112 | 113 | Defaults to django.core.files.storage.FileSystemStorage. 114 | 115 | Any custom storage class used here must either be a subclass of 116 | django.core.files.storage.FileSystemStorage, or accept a location 117 | init parameter. 118 | """ 119 | if not chunks_upload_to: 120 | chunks_upload_to = get_chunks_subdir() 121 | location = os.path.join(settings.MEDIA_ROOT, chunks_upload_to) 122 | url_path = urljoin(settings.MEDIA_URL, chunks_upload_to) 123 | ensure_dir(location) 124 | storage_class_name = getattr( 125 | settings, 126 | 'RESUMABLE_STORAGE', 127 | 'django.core.files.storage.FileSystemStorage' 128 | ) 129 | return get_storage_class(storage_class_name)( 130 | location=location, base_url=url_path) 131 | 132 | 133 | def get_chunks_upload_to(request): 134 | if request.method == 'POST': 135 | ct_id = request.POST['content_type_id'] 136 | field_name = request.POST['field_name'] 137 | else: 138 | ct_id = request.GET['content_type_id'] 139 | field_name = request.GET['field_name'] 140 | 141 | ct = ContentType.objects.get_for_id(ct_id) 142 | model_cls = ct.model_class() 143 | field = model_cls._meta.get_field(field_name) 144 | return field.chunks_upload_to 145 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django 2.x Resumable (`django2-resumable`) 2 | 3 | ``django2-resumable`` provides **Django 2.1** backend stuff (e.g. `ModelFields`, `Forms`, `staticfiles`) 4 | to integrates [`resumable.js`]() in Django apps and admin. 5 | 6 | This projects build on the original `django-resumable` by [jeanphix](https://github.com/jeanphix/django-resumable), which (_afaik_) it 7 | is not maintained anymore, and does not support Django 2.x - _main 8 | reason why I ended up developing this in the first place_ (ed.) 9 | 10 | #### `ICYM`: 11 | 12 | (from the [documentation](https://github.com/23/resumable.js/blob/master/README.md)) 13 | 14 | >Resumable.js is a JavaScript library providing multiple simultaneous, stable and 15 | >resumable uploads via the [`HTML5 File API`](http://www.w3.org/TR/FileAPI/). 16 | > 17 | >The library is designed to introduce fault-tolerance into the upload of large files through HTTP. 18 | >This is done by splitting each file into small chunks. 19 | >Then, whenever the upload of a chunk fails, uploading is retried until the procedure completes. 20 | >This allows uploads to automatically resume uploading after a network connection 21 | >is lost either locally or to the server. 22 | >Additionally, it allows for users to pause, resume and even recover uploads without 23 | >losing state because only the currently uploading chunks will be aborted, not the entire upload. 24 | > 25 | >Resumable.js does not have any external dependencies other than the `HTML5 File API`. 26 | >This is relied on for the ability to chunk files into smaller pieces. 27 | >Currently, this means that support is widely available in to Firefox 4+, Chrome 11+, 28 | >Safari 6+ and Internet Explorer 10+. 29 | 30 | 31 | ## Installation 32 | 33 | * ``pip install django2-resumable`` 34 | * Add ``django2_resumable`` to your ``INSTALLED_APPS`` 35 | 36 | ## How to use 37 | 38 | ### Views 39 | 40 | In order to enable asynchronous files upload files, you must define an endpoint that will deal 41 | with uploaded file chunks: 42 | 43 | ```Python 44 | from django.urls import path, include 45 | 46 | urlpatterns = [ 47 | path('resumable_upload/', include('django2_resumable.urls')), 48 | ] 49 | ``` 50 | 51 | By default, the `resume-upload` view is provided with no restriction on the accesses 52 | (i.e. no `login_required` nor `staff_member_required`). 53 | 54 | To enable the view on restricted levels of permissions, urls should be modified accordingly: 55 | 56 | ```Python 57 | 58 | from django.contrib.auth.views import login_required 59 | # To enable view in AdminForm 60 | from django.contrib.admin.views.decorators import staff_member_required 61 | 62 | from django2_resumable.views import resumable_upload 63 | from django.urls import path 64 | 65 | urlpatterns = [ 66 | path('resumable-upload', login_required(resumable_upload), 67 | name='resumable-upload'), 68 | path('admin-resumable-upload', staff_member_required(resumable_upload), 69 | name='admin-resumable-upload'), 70 | ] 71 | 72 | ``` 73 | 74 | ### Model 75 | 76 | `django2-resumable` provides a `ResumableFileField` that can be easily 77 | integrated in your Model class: 78 | 79 | ```Python 80 | 81 | from django.db import models 82 | from django2_resumable.fields import ResumableFileField 83 | 84 | class MyModel(models.Model): 85 | file = ResumableFileField(chunks_upload_to='resumable_chunks', **kwargs) 86 | ``` 87 | 88 | The `ResumableFileField` field extends the default `django.core.fields.FileField` by including 89 | an additional parameter, namely `chunks_upload_to` specifying the path in the `MEDIA_ROOT` in which 90 | temporary chunks will be uploaded. Once the upload is complete, the file will be 91 | automatically moved to the `upload_to` destination folder (if any). 92 | 93 | 94 | ### Form 95 | 96 | If you want to handle resumable upload within your forms, 97 | you can use the `FormResumableFileField`: 98 | 99 | ```Python 100 | from django.forms import Form 101 | from django2_resumable.forms import FormResumableFileField 102 | 103 | 104 | class ResumableForm(Form): 105 | file = FormResumableFileField() 106 | 107 | ``` 108 | 109 | It is as simple as that: 110 | `FormResumableFileField` simply extends the core `django.forms.FileField` by injecting the 111 | `django_resumable.widgets.ResumableWidget`. 112 | This widget is the default widget mapped by default to `ResumableFileField` instances 113 | (see `django_resumable.fields.ResumableFileField.formfield` method). 114 | 115 | 116 | ### Additional Settings 117 | 118 | ``django2-resumable`` comes with some extendable settings allowing for additional setup: 119 | 120 | - `RESUMABLE_SUBDIR`: Directory in `MEDIA_ROOT` in which chunks will be uploaded. This settings will be 121 | overriden by any `chunks_upload_to` options specified at the time of definition of 122 | `ResumableFileField` within Django Model. 123 | 124 | - `RESUMABLE_STORAGE`: (default `django.core.files.storage.FileSystemStorage`) 125 | Django Storage class to be used to handle the uploads. 126 | -------------------------------------------------------------------------------- /django2_resumable/static/django2_resumable/js/resumable.js: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT Licensed 3 | * http://www.23developer.com/opensource 4 | * http://github.com/23/resumable.js 5 | * Steffen Tiedemann Christensen, steffen@23company.com 6 | */ 7 | 8 | (function(){ 9 | "use strict"; 10 | 11 | var Resumable = function(opts){ 12 | if ( !(this instanceof Resumable) ) { 13 | return new Resumable(opts); 14 | } 15 | this.version = 1.0; 16 | // SUPPORTED BY BROWSER? 17 | // Check if these features are support by the browser: 18 | // - File object type 19 | // - Blob object type 20 | // - FileList object type 21 | // - slicing files 22 | this.support = ( 23 | (typeof(File)!=='undefined') 24 | && 25 | (typeof(Blob)!=='undefined') 26 | && 27 | (typeof(FileList)!=='undefined') 28 | && 29 | (!!Blob.prototype.webkitSlice||!!Blob.prototype.mozSlice||!!Blob.prototype.slice||false) 30 | ); 31 | if(!this.support) return(false); 32 | 33 | 34 | // PROPERTIES 35 | var $ = this; 36 | $.files = []; 37 | $.defaults = { 38 | chunkSize:1*1024*1024, 39 | forceChunkSize:false, 40 | simultaneousUploads:3, 41 | fileParameterName:'file', 42 | throttleProgressCallbacks:0.5, 43 | query:{}, 44 | headers:{}, 45 | preprocess:null, 46 | method:'multipart', 47 | uploadMethod: 'POST', 48 | testMethod: 'GET', 49 | prioritizeFirstAndLastChunk:false, 50 | target:'/', 51 | parameterNamespace:'', 52 | testChunks:true, 53 | generateUniqueIdentifier:null, 54 | getTarget:null, 55 | maxChunkRetries:undefined, 56 | chunkRetryInterval:undefined, 57 | permanentErrors:[400, 404, 415, 500, 501], 58 | maxFiles:undefined, 59 | withCredentials:false, 60 | xhrTimeout:0, 61 | maxFilesErrorCallback:function (files, errorCount) { 62 | var maxFiles = $.getOpt('maxFiles'); 63 | alert('Please upload ' + maxFiles + ' file' + (maxFiles === 1 ? '' : 's') + ' at a time.'); 64 | }, 65 | minFileSize:1, 66 | minFileSizeErrorCallback:function(file, errorCount) { 67 | alert(file.fileName||file.name +' is too small, please upload files larger than ' + $h.formatSize($.getOpt('minFileSize')) + '.'); 68 | }, 69 | maxFileSize:undefined, 70 | maxFileSizeErrorCallback:function(file, errorCount) { 71 | alert(file.fileName||file.name +' is too large, please upload files less than ' + $h.formatSize($.getOpt('maxFileSize')) + '.'); 72 | }, 73 | fileType: [], 74 | fileTypeErrorCallback: function(file, errorCount) { 75 | alert(file.fileName||file.name +' has type not allowed, please upload files of type ' + $.getOpt('fileType') + '.'); 76 | } 77 | }; 78 | $.opts = opts||{}; 79 | $.getOpt = function(o) { 80 | var $opt = this; 81 | // Get multiple option if passed an array 82 | if(o instanceof Array) { 83 | var options = {}; 84 | $h.each(o, function(option){ 85 | options[option] = $opt.getOpt(option); 86 | }); 87 | return options; 88 | } 89 | // Otherwise, just return a simple option 90 | if ($opt instanceof ResumableChunk) { 91 | if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; } 92 | else { $opt = $opt.fileObj; } 93 | } 94 | if ($opt instanceof ResumableFile) { 95 | if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; } 96 | else { $opt = $opt.resumableObj; } 97 | } 98 | if ($opt instanceof Resumable) { 99 | if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; } 100 | else { return $opt.defaults[o]; } 101 | } 102 | }; 103 | 104 | // EVENTS 105 | // catchAll(event, ...) 106 | // fileSuccess(file), fileProgress(file), fileAdded(file, event), fileRetry(file), fileError(file, message), 107 | // complete(), progress(), error(message, file), pause() 108 | $.events = []; 109 | $.on = function(event,callback){ 110 | $.events.push(event.toLowerCase(), callback); 111 | }; 112 | $.fire = function(){ 113 | // `arguments` is an object, not array, in FF, so: 114 | var args = []; 115 | for (var i=0; i0){ 293 | //add these results to the array of all the new stuff 294 | for (var i=0; i 0){ 367 | var fileTypeFound = false; 368 | for(var index in o.fileType){ 369 | var extension = '.' + o.fileType[index]; 370 | if(fileName.indexOf(extension, fileName.length - extension.length) !== -1){ 371 | fileTypeFound = true; 372 | break; 373 | } 374 | } 375 | if (!fileTypeFound) { 376 | o.fileTypeErrorCallback(file, errorCount++); 377 | return false; 378 | } 379 | } 380 | 381 | if (typeof(o.minFileSize)!=='undefined' && file.sizeo.maxFileSize) { 386 | o.maxFileSizeErrorCallback(file, errorCount++); 387 | return false; 388 | } 389 | 390 | function addFile(uniqueIdentifier){ 391 | if (!$.getFromUniqueIdentifier(uniqueIdentifier)) {(function(){ 392 | file.uniqueIdentifier = uniqueIdentifier; 393 | var f = new ResumableFile($, file, uniqueIdentifier); 394 | $.files.push(f); 395 | files.push(f); 396 | f.container = (typeof event != 'undefined' ? event.srcElement : null); 397 | window.setTimeout(function(){ 398 | $.fire('fileAdded', f, event) 399 | },0); 400 | })()}; 401 | } 402 | // directories have size == 0 403 | var uniqueIdentifier = $h.generateUniqueIdentifier(file) 404 | if(uniqueIdentifier && typeof uniqueIdentifier.done === 'function' && typeof uniqueIdentifier.fail === 'function'){ 405 | uniqueIdentifier 406 | .done(function(uniqueIdentifier){ 407 | addFile(uniqueIdentifier); 408 | }) 409 | .fail(function(){ 410 | addFile(); 411 | }); 412 | }else{ 413 | addFile(uniqueIdentifier); 414 | } 415 | 416 | }); 417 | window.setTimeout(function(){ 418 | $.fire('filesAdded', files) 419 | },0); 420 | }; 421 | 422 | // INTERNAL OBJECT TYPES 423 | function ResumableFile(resumableObj, file, uniqueIdentifier){ 424 | var $ = this; 425 | $.opts = {}; 426 | $.getOpt = resumableObj.getOpt; 427 | $._prevProgress = 0; 428 | $.resumableObj = resumableObj; 429 | $.file = file; 430 | $.fileName = file.fileName||file.name; // Some confusion in different versions of Firefox 431 | $.size = file.size; 432 | $.relativePath = file.webkitRelativePath || file.relativePath || $.fileName; 433 | $.uniqueIdentifier = uniqueIdentifier; 434 | $._pause = false; 435 | $.container = ''; 436 | var _error = uniqueIdentifier !== undefined; 437 | 438 | // Callback when something happens within the chunk 439 | var chunkEvent = function(event, message){ 440 | // event can be 'progress', 'success', 'error' or 'retry' 441 | switch(event){ 442 | case 'progress': 443 | $.resumableObj.fire('fileProgress', $); 444 | break; 445 | case 'error': 446 | $.abort(); 447 | _error = true; 448 | $.chunks = []; 449 | $.resumableObj.fire('fileError', $, message); 450 | break; 451 | case 'success': 452 | if(_error) return; 453 | $.resumableObj.fire('fileProgress', $); // it's at least progress 454 | if($.isComplete()) { 455 | $.resumableObj.fire('fileSuccess', $, message); 456 | } 457 | break; 458 | case 'retry': 459 | $.resumableObj.fire('fileRetry', $); 460 | break; 461 | } 462 | }; 463 | 464 | // Main code to set up a file object with chunks, 465 | // packaged to be able to handle retries if needed. 466 | $.chunks = []; 467 | $.abort = function(){ 468 | // Stop current uploads 469 | var abortCount = 0; 470 | $h.each($.chunks, function(c){ 471 | if(c.status()=='uploading') { 472 | c.abort(); 473 | abortCount++; 474 | } 475 | }); 476 | if(abortCount>0) $.resumableObj.fire('fileProgress', $); 477 | }; 478 | $.cancel = function(){ 479 | // Reset this file to be void 480 | var _chunks = $.chunks; 481 | $.chunks = []; 482 | // Stop current uploads 483 | $h.each(_chunks, function(c){ 484 | if(c.status()=='uploading') { 485 | c.abort(); 486 | $.resumableObj.uploadNextChunk(); 487 | } 488 | }); 489 | $.resumableObj.removeFile($); 490 | $.resumableObj.fire('fileProgress', $); 491 | }; 492 | $.retry = function(){ 493 | $.bootstrap(); 494 | var firedRetry = false; 495 | $.resumableObj.on('chunkingComplete', function(){ 496 | if(!firedRetry) $.resumableObj.upload(); 497 | firedRetry = true; 498 | }); 499 | }; 500 | $.bootstrap = function(){ 501 | $.abort(); 502 | _error = false; 503 | // Rebuild stack of chunks from file 504 | $.chunks = []; 505 | $._prevProgress = 0; 506 | var round = $.getOpt('forceChunkSize') ? Math.ceil : Math.floor; 507 | var maxOffset = Math.max(round($.file.size/$.getOpt('chunkSize')),1); 508 | for (var offset=0; offset0.99999 ? 1 : ret)); 528 | ret = Math.max($._prevProgress, ret); // We don't want to lose percentages when an upload is paused 529 | $._prevProgress = ret; 530 | return(ret); 531 | }; 532 | $.isUploading = function(){ 533 | var uploading = false; 534 | $h.each($.chunks, function(chunk){ 535 | if(chunk.status()=='uploading') { 536 | uploading = true; 537 | return(false); 538 | } 539 | }); 540 | return(uploading); 541 | }; 542 | $.isComplete = function(){ 543 | var outstanding = false; 544 | $h.each($.chunks, function(chunk){ 545 | var status = chunk.status(); 546 | if(status=='pending' || status=='uploading' || chunk.preprocessState === 1) { 547 | outstanding = true; 548 | return(false); 549 | } 550 | }); 551 | return(!outstanding); 552 | }; 553 | $.pause = function(pause){ 554 | if(typeof(pause)==='undefined'){ 555 | $._pause = ($._pause ? false : true); 556 | }else{ 557 | $._pause = pause; 558 | } 559 | }; 560 | $.isPaused = function() { 561 | return $._pause; 562 | }; 563 | 564 | 565 | // Bootstrap and return 566 | $.resumableObj.fire('chunkingStart', $); 567 | $.bootstrap(); 568 | return(this); 569 | } 570 | 571 | 572 | function ResumableChunk(resumableObj, fileObj, offset, callback){ 573 | var $ = this; 574 | $.opts = {}; 575 | $.getOpt = resumableObj.getOpt; 576 | $.resumableObj = resumableObj; 577 | $.fileObj = fileObj; 578 | $.fileObjSize = fileObj.size; 579 | $.fileObjType = fileObj.file.type; 580 | $.offset = offset; 581 | $.callback = callback; 582 | $.lastProgressCallback = (new Date); 583 | $.tested = false; 584 | $.retries = 0; 585 | $.pendingRetry = false; 586 | $.preprocessState = 0; // 0 = unprocessed, 1 = processing, 2 = finished 587 | 588 | // Computed properties 589 | var chunkSize = $.getOpt('chunkSize'); 590 | $.loaded = 0; 591 | $.startByte = $.offset*chunkSize; 592 | $.endByte = Math.min($.fileObjSize, ($.offset+1)*chunkSize); 593 | if ($.fileObjSize-$.endByte < chunkSize && !$.getOpt('forceChunkSize')) { 594 | // The last chunk will be bigger than the chunk size, but less than 2*chunkSize 595 | $.endByte = $.fileObjSize; 596 | } 597 | $.xhr = null; 598 | 599 | // test() makes a GET request without any data to see if the chunk has already been uploaded in a previous session 600 | $.test = function(){ 601 | // Set up request and listen for event 602 | $.xhr = new XMLHttpRequest(); 603 | 604 | var testHandler = function(e){ 605 | $.tested = true; 606 | var status = $.status(); 607 | if(status=='success') { 608 | $.callback(status, $.message()); 609 | $.resumableObj.uploadNextChunk(); 610 | } else { 611 | $.send(); 612 | } 613 | }; 614 | $.xhr.addEventListener('load', testHandler, false); 615 | $.xhr.addEventListener('error', testHandler, false); 616 | $.xhr.addEventListener('timeout', testHandler, false); 617 | 618 | // Add data from the query options 619 | var params = []; 620 | var parameterNamespace = $.getOpt('parameterNamespace'); 621 | var customQuery = $.getOpt('query'); 622 | if(typeof customQuery == 'function') customQuery = customQuery($.fileObj, $); 623 | $h.each(customQuery, function(k,v){ 624 | params.push([encodeURIComponent(parameterNamespace+k), encodeURIComponent(v)].join('=')); 625 | }); 626 | // Add extra data to identify chunk 627 | params.push([parameterNamespace+'resumableChunkNumber', encodeURIComponent($.offset+1)].join('=')); 628 | params.push([parameterNamespace+'resumableChunkSize', encodeURIComponent($.getOpt('chunkSize'))].join('=')); 629 | params.push([parameterNamespace+'resumableCurrentChunkSize', encodeURIComponent($.endByte - $.startByte)].join('=')); 630 | params.push([parameterNamespace+'resumableTotalSize', encodeURIComponent($.fileObjSize)].join('=')); 631 | params.push([parameterNamespace+'resumableType', encodeURIComponent($.fileObjType)].join('=')); 632 | params.push([parameterNamespace+'resumableIdentifier', encodeURIComponent($.fileObj.uniqueIdentifier)].join('=')); 633 | params.push([parameterNamespace+'resumableFilename', encodeURIComponent($.fileObj.fileName)].join('=')); 634 | params.push([parameterNamespace+'resumableRelativePath', encodeURIComponent($.fileObj.relativePath)].join('=')); 635 | params.push([parameterNamespace+'resumableTotalChunks', encodeURIComponent($.fileObj.chunks.length)].join('=')); 636 | // Append the relevant chunk and send it 637 | $.xhr.open($.getOpt('testMethod'), $h.getTarget(params)); 638 | $.xhr.timeout = $.getOpt('xhrTimeout'); 639 | $.xhr.withCredentials = $.getOpt('withCredentials'); 640 | // Add data from header options 641 | $h.each($.getOpt('headers'), function(k,v) { 642 | $.xhr.setRequestHeader(k, v); 643 | }); 644 | $.xhr.send(null); 645 | }; 646 | 647 | $.preprocessFinished = function(){ 648 | $.preprocessState = 2; 649 | $.send(); 650 | }; 651 | 652 | // send() uploads the actual data in a POST call 653 | $.send = function(){ 654 | var preprocess = $.getOpt('preprocess'); 655 | if(typeof preprocess === 'function') { 656 | switch($.preprocessState) { 657 | case 0: $.preprocessState = 1; preprocess($); return; 658 | case 1: return; 659 | case 2: break; 660 | } 661 | } 662 | if($.getOpt('testChunks') && !$.tested) { 663 | $.test(); 664 | return; 665 | } 666 | 667 | // Set up request and listen for event 668 | $.xhr = new XMLHttpRequest(); 669 | 670 | // Progress 671 | $.xhr.upload.addEventListener('progress', function(e){ 672 | if( (new Date) - $.lastProgressCallback > $.getOpt('throttleProgressCallbacks') * 1000 ) { 673 | $.callback('progress'); 674 | $.lastProgressCallback = (new Date); 675 | } 676 | $.loaded=e.loaded||0; 677 | }, false); 678 | $.loaded = 0; 679 | $.pendingRetry = false; 680 | $.callback('progress'); 681 | 682 | // Done (either done, failed or retry) 683 | var doneHandler = function(e){ 684 | var status = $.status(); 685 | if(status=='success'||status=='error') { 686 | $.callback(status, $.message()); 687 | $.resumableObj.uploadNextChunk(); 688 | } else { 689 | $.callback('retry', $.message()); 690 | $.abort(); 691 | $.retries++; 692 | var retryInterval = $.getOpt('chunkRetryInterval'); 693 | if(retryInterval !== undefined) { 694 | $.pendingRetry = true; 695 | setTimeout($.send, retryInterval); 696 | } else { 697 | $.send(); 698 | } 699 | } 700 | }; 701 | $.xhr.addEventListener('load', doneHandler, false); 702 | $.xhr.addEventListener('error', doneHandler, false); 703 | $.xhr.addEventListener('timeout', doneHandler, false); 704 | 705 | // Set up the basic query data from Resumable 706 | var query = { 707 | resumableChunkNumber: $.offset+1, 708 | resumableChunkSize: $.getOpt('chunkSize'), 709 | resumableCurrentChunkSize: $.endByte - $.startByte, 710 | resumableTotalSize: $.fileObjSize, 711 | resumableType: $.fileObjType, 712 | resumableIdentifier: $.fileObj.uniqueIdentifier, 713 | resumableFilename: $.fileObj.fileName, 714 | resumableRelativePath: $.fileObj.relativePath, 715 | resumableTotalChunks: $.fileObj.chunks.length 716 | }; 717 | // Mix in custom data 718 | var customQuery = $.getOpt('query'); 719 | if(typeof customQuery == 'function') customQuery = customQuery($.fileObj, $); 720 | $h.each(customQuery, function(k,v){ 721 | query[k] = v; 722 | }); 723 | 724 | var func = ($.fileObj.file.slice ? 'slice' : ($.fileObj.file.mozSlice ? 'mozSlice' : ($.fileObj.file.webkitSlice ? 'webkitSlice' : 'slice'))), 725 | bytes = $.fileObj.file[func]($.startByte,$.endByte), 726 | data = null, 727 | target = $.getOpt('target'); 728 | 729 | var parameterNamespace = $.getOpt('parameterNamespace'); 730 | if ($.getOpt('method') === 'octet') { 731 | // Add data from the query options 732 | data = bytes; 733 | var params = []; 734 | $h.each(query, function(k,v){ 735 | params.push([encodeURIComponent(parameterNamespace+k), encodeURIComponent(v)].join('=')); 736 | }); 737 | target = $h.getTarget(params); 738 | } else { 739 | // Add data from the query options 740 | data = new FormData(); 741 | $h.each(query, function(k,v){ 742 | data.append(parameterNamespace+k,v); 743 | }); 744 | data.append(parameterNamespace+$.getOpt('fileParameterName'), bytes); 745 | } 746 | 747 | var method = $.getOpt('uploadMethod'); 748 | $.xhr.open(method, target); 749 | if ($.getOpt('method') === 'octet') { 750 | $.xhr.setRequestHeader('Content-Type', 'binary/octet-stream'); 751 | } 752 | $.xhr.timeout = $.getOpt('xhrTimeout'); 753 | $.xhr.withCredentials = $.getOpt('withCredentials'); 754 | // Add data from header options 755 | $h.each($.getOpt('headers'), function(k,v) { 756 | $.xhr.setRequestHeader(k, v); 757 | }); 758 | $.xhr.send(data); 759 | }; 760 | $.abort = function(){ 761 | // Abort and reset 762 | if($.xhr) $.xhr.abort(); 763 | $.xhr = null; 764 | }; 765 | $.status = function(){ 766 | // Returns: 'pending', 'uploading', 'success', 'error' 767 | if($.pendingRetry) { 768 | // if pending retry then that's effectively the same as actively uploading, 769 | // there might just be a slight delay before the retry starts 770 | return('uploading'); 771 | } else if(!$.xhr) { 772 | return('pending'); 773 | } else if($.xhr.readyState<4) { 774 | // Status is really 'OPENED', 'HEADERS_RECEIVED' or 'LOADING' - meaning that stuff is happening 775 | return('uploading'); 776 | } else { 777 | if($.xhr.status == 200 || $.xhr.status == 201) { 778 | // HTTP 200 or 201 (created) perfect 779 | return('success'); 780 | } else if($h.contains($.getOpt('permanentErrors'), $.xhr.status) || $.retries >= $.getOpt('maxChunkRetries')) { 781 | // HTTP 415/500/501, permanent error 782 | return('error'); 783 | } else { 784 | // this should never happen, but we'll reset and queue a retry 785 | // a likely case for this would be 503 service unavailable 786 | $.abort(); 787 | return('pending'); 788 | } 789 | } 790 | }; 791 | $.message = function(){ 792 | return($.xhr ? $.xhr.responseText : ''); 793 | }; 794 | $.progress = function(relative){ 795 | if(typeof(relative)==='undefined') relative = false; 796 | var factor = (relative ? ($.endByte-$.startByte)/$.fileObjSize : 1); 797 | if($.pendingRetry) return(0); 798 | var s = $.status(); 799 | switch(s){ 800 | case 'success': 801 | case 'error': 802 | return(1*factor); 803 | case 'pending': 804 | return(0*factor); 805 | default: 806 | return($.loaded/($.endByte-$.startByte)*factor); 807 | } 808 | }; 809 | return(this); 810 | } 811 | 812 | // QUEUE 813 | $.uploadNextChunk = function(){ 814 | var found = false; 815 | 816 | // In some cases (such as videos) it's really handy to upload the first 817 | // and last chunk of a file quickly; this let's the server check the file's 818 | // metadata and determine if there's even a point in continuing. 819 | if ($.getOpt('prioritizeFirstAndLastChunk')) { 820 | $h.each($.files, function(file){ 821 | if(file.chunks.length && file.chunks[0].status()=='pending' && file.chunks[0].preprocessState === 0) { 822 | file.chunks[0].send(); 823 | found = true; 824 | return(false); 825 | } 826 | if(file.chunks.length>1 && file.chunks[file.chunks.length-1].status()=='pending' && file.chunks[file.chunks.length-1].preprocessState === 0) { 827 | file.chunks[file.chunks.length-1].send(); 828 | found = true; 829 | return(false); 830 | } 831 | }); 832 | if(found) return(true); 833 | } 834 | 835 | // Now, simply look for the next, best thing to upload 836 | $h.each($.files, function(file){ 837 | if(file.isPaused()===false){ 838 | $h.each(file.chunks, function(chunk){ 839 | if(chunk.status()=='pending' && chunk.preprocessState === 0) { 840 | chunk.send(); 841 | found = true; 842 | return(false); 843 | } 844 | }); 845 | } 846 | if(found) return(false); 847 | }); 848 | if(found) return(true); 849 | 850 | // The are no more outstanding chunks to upload, check is everything is done 851 | var outstanding = false; 852 | $h.each($.files, function(file){ 853 | if(!file.isComplete()) { 854 | outstanding = true; 855 | return(false); 856 | } 857 | }); 858 | if(!outstanding) { 859 | // All chunks have been uploaded, complete 860 | $.fire('complete'); 861 | } 862 | return(false); 863 | }; 864 | 865 | 866 | // PUBLIC METHODS FOR RESUMABLE.JS 867 | $.assignBrowse = function(domNodes, isDirectory){ 868 | if(typeof(domNodes.length)=='undefined') domNodes = [domNodes]; 869 | 870 | $h.each(domNodes, function(domNode) { 871 | var input; 872 | if(domNode.tagName==='INPUT' && domNode.type==='file'){ 873 | input = domNode; 874 | } else { 875 | input = document.createElement('input'); 876 | input.setAttribute('type', 'file'); 877 | input.style.display = 'none'; 878 | domNode.addEventListener('click', function(){ 879 | input.style.opacity = 0; 880 | input.style.display='block'; 881 | input.focus(); 882 | input.click(); 883 | input.style.display='none'; 884 | }, false); 885 | domNode.appendChild(input); 886 | } 887 | var maxFiles = $.getOpt('maxFiles'); 888 | if (typeof(maxFiles)==='undefined'||maxFiles!=1){ 889 | input.setAttribute('multiple', 'multiple'); 890 | } else { 891 | input.removeAttribute('multiple'); 892 | } 893 | if(isDirectory){ 894 | input.setAttribute('webkitdirectory', 'webkitdirectory'); 895 | } else { 896 | input.removeAttribute('webkitdirectory'); 897 | } 898 | // When new files are added, simply append them to the overall list 899 | input.addEventListener('change', function(e){ 900 | appendFilesFromFileList(e.target.files,e); 901 | e.target.value = ''; 902 | }, false); 903 | }); 904 | }; 905 | $.assignDrop = function(domNodes){ 906 | if(typeof(domNodes.length)=='undefined') domNodes = [domNodes]; 907 | 908 | $h.each(domNodes, function(domNode) { 909 | domNode.addEventListener('dragover', preventDefault, false); 910 | domNode.addEventListener('dragenter', preventDefault, false); 911 | domNode.addEventListener('drop', onDrop, false); 912 | }); 913 | }; 914 | $.unAssignDrop = function(domNodes) { 915 | if (typeof(domNodes.length) == 'undefined') domNodes = [domNodes]; 916 | 917 | $h.each(domNodes, function(domNode) { 918 | domNode.removeEventListener('dragover', preventDefault); 919 | domNode.removeEventListener('dragenter', preventDefault); 920 | domNode.removeEventListener('drop', onDrop); 921 | }); 922 | }; 923 | $.isUploading = function(){ 924 | var uploading = false; 925 | $h.each($.files, function(file){ 926 | if (file.isUploading()) { 927 | uploading = true; 928 | return(false); 929 | } 930 | }); 931 | return(uploading); 932 | }; 933 | $.upload = function(){ 934 | // Make sure we don't start too many uploads at once 935 | if($.isUploading()) return; 936 | // Kick off the queue 937 | $.fire('uploadStart'); 938 | for (var num=1; num<=$.getOpt('simultaneousUploads'); num++) { 939 | $.uploadNextChunk(); 940 | } 941 | }; 942 | $.pause = function(){ 943 | // Resume all chunks currently being uploaded 944 | $h.each($.files, function(file){ 945 | file.abort(); 946 | }); 947 | $.fire('pause'); 948 | }; 949 | $.cancel = function(){ 950 | for(var i = $.files.length - 1; i >= 0; i--) { 951 | $.files[i].cancel(); 952 | } 953 | $.fire('cancel'); 954 | }; 955 | $.progress = function(){ 956 | var totalDone = 0; 957 | var totalSize = 0; 958 | // Resume all chunks currently being uploaded 959 | $h.each($.files, function(file){ 960 | totalDone += file.progress()*file.size; 961 | totalSize += file.size; 962 | }); 963 | return(totalSize>0 ? totalDone/totalSize : 0); 964 | }; 965 | $.addFile = function(file, event){ 966 | appendFilesFromFileList([file], event); 967 | }; 968 | $.removeFile = function(file){ 969 | for(var i = $.files.length - 1; i >= 0; i--) { 970 | if($.files[i] === file) { 971 | $.files.splice(i, 1); 972 | } 973 | } 974 | }; 975 | $.getFromUniqueIdentifier = function(uniqueIdentifier){ 976 | var ret = false; 977 | $h.each($.files, function(f){ 978 | if(f.uniqueIdentifier==uniqueIdentifier) ret = f; 979 | }); 980 | return(ret); 981 | }; 982 | $.getSize = function(){ 983 | var totalSize = 0; 984 | $h.each($.files, function(file){ 985 | totalSize += file.size; 986 | }); 987 | return(totalSize); 988 | }; 989 | 990 | return(this); 991 | }; 992 | 993 | 994 | // Node.js-style export for Node and Component 995 | if (typeof module != 'undefined') { 996 | module.exports = Resumable; 997 | } else if (typeof define === "function" && define.amd) { 998 | // AMD/requirejs: Define the module 999 | define(function(){ 1000 | return Resumable; 1001 | }); 1002 | } else { 1003 | // Browser: Expose to window 1004 | window.Resumable = Resumable; 1005 | } 1006 | 1007 | })(); 1008 | --------------------------------------------------------------------------------