├── 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 |
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 |
--------------------------------------------------------------------------------