1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim:set ts=2 sw=2 sts=2 et cindent: */
3 /* ***** BEGIN LICENSE BLOCK *****
4 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
6 * The contents of this file are subject to the Mozilla Public License Version
7 * 1.1 (the "License"); you may not use this file except in compliance with
8 * the License. You may obtain a copy of the License at
9 * http://www.mozilla.org/MPL/
11 * Software distributed under the License is distributed on an "AS IS" basis,
12 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
13 * for the specific language governing rights and limitations under the
16 * The Original Code is mozilla.org code.
18 * The Initial Developer of the Original Code is Google Inc.
19 * Portions created by the Initial Developer are Copyright (C) 2005
20 * the Initial Developer. All Rights Reserved.
23 * Darin Fisher <darin@meer.net>
25 * Alternatively, the contents of this file may be used under the terms of
26 * either the GNU General Public License Version 2 or later (the "GPL"), or
27 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
28 * in which case the provisions of the GPL or the LGPL are applicable instead
29 * of those above. If you wish to allow use of your version of this file only
30 * under the terms of either the GPL or the LGPL, and not to allow others to
31 * use your version of this file under the terms of the MPL, indicate your
32 * decision by deleting the provisions above and replace them with the notice
33 * and other provisions required by the GPL or the LGPL. If you do not delete
34 * the provisions above, a recipient may use your version of this file under
35 * the terms of any one of the MPL, the GPL or the LGPL.
37 * ***** END LICENSE BLOCK ***** */
39 #include "nsIIncrementalDownload.h"
40 #include "nsIRequestObserver.h"
41 #include "nsIProgressEventSink.h"
42 #include "nsIChannelEventSink.h"
43 #include "nsIInterfaceRequestor.h"
44 #include "nsIObserverService.h"
45 #include "nsIObserver.h"
46 #include "nsIPropertyBag2.h"
47 #include "nsIServiceManager.h"
48 #include "nsILocalFile.h"
51 #include "nsNetUtil.h"
52 #include "nsAutoPtr.h"
53 #include "nsWeakReference.h"
54 #include "nsChannelProperties.h"
58 // Error code used internally by the incremental downloader to cancel the
59 // network channel when the download is already complete.
60 #define NS_ERROR_DOWNLOAD_COMPLETE \
61 NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_GENERAL, 1)
63 // Error code used internally by the incremental downloader to cancel the
64 // network channel when the response to a range request is 200 instead of 206.
65 #define NS_ERROR_DOWNLOAD_NOT_PARTIAL \
66 NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_GENERAL, 2)
68 // Default values used to initialize a nsIncrementalDownload object.
69 #define DEFAULT_CHUNK_SIZE (4096 * 16) // bytes
70 #define DEFAULT_INTERVAL 60 // seconds
72 // Number of times to retry a failed byte-range request.
73 #define MAX_RETRY_COUNT 20
75 //-----------------------------------------------------------------------------
78 WriteToFile(nsILocalFile
*lf
, const char *data
, PRUint32 len
, PRInt32 flags
)
81 nsresult rv
= lf
->OpenNSPRFileDesc(flags
, 0600, &fd
);
86 rv
= PR_Write(fd
, data
, len
) == PRInt32(len
) ? NS_OK
: NS_ERROR_FAILURE
;
93 AppendToFile(nsILocalFile
*lf
, const char *data
, PRUint32 len
)
95 PRInt32 flags
= PR_WRONLY
| PR_CREATE_FILE
| PR_APPEND
;
96 return WriteToFile(lf
, data
, len
, flags
);
99 // maxSize may be -1 if unknown
101 MakeRangeSpec(const nsInt64
&size
, const nsInt64
&maxSize
, PRInt32 chunkSize
,
102 PRBool fetchRemaining
, nsCString
&rangeSpec
)
104 rangeSpec
.AssignLiteral("bytes=");
105 rangeSpec
.AppendInt(PRInt64(size
));
106 rangeSpec
.Append('-');
111 nsInt64 end
= size
+ nsInt64(chunkSize
);
112 if (maxSize
!= nsInt64(-1) && end
> maxSize
)
116 rangeSpec
.AppendInt(PRInt64(end
));
119 //-----------------------------------------------------------------------------
121 class nsIncrementalDownload
: public nsIIncrementalDownload
122 , public nsIStreamListener
124 , public nsIInterfaceRequestor
125 , public nsIChannelEventSink
126 , public nsSupportsWeakReference
131 NS_DECL_NSIINCREMENTALDOWNLOAD
132 NS_DECL_NSIREQUESTOBSERVER
133 NS_DECL_NSISTREAMLISTENER
135 NS_DECL_NSIINTERFACEREQUESTOR
136 NS_DECL_NSICHANNELEVENTSINK
138 nsIncrementalDownload();
141 ~nsIncrementalDownload() {}
142 nsresult
FlushChunk();
143 nsresult
CallOnStartRequest();
144 void CallOnStopRequest();
145 nsresult
StartTimer(PRInt32 interval
);
146 nsresult
ProcessTimeout();
147 nsresult
ReadCurrentSize();
148 nsresult
ClearRequestHeader(nsIHttpChannel
*channel
);
150 nsCOMPtr
<nsIRequestObserver
> mObserver
;
151 nsCOMPtr
<nsISupports
> mObserverContext
;
152 nsCOMPtr
<nsIProgressEventSink
> mProgressSink
;
153 nsCOMPtr
<nsIURI
> mURI
;
154 nsCOMPtr
<nsIURI
> mFinalURI
;
155 nsCOMPtr
<nsILocalFile
> mDest
;
156 nsCOMPtr
<nsIChannel
> mChannel
;
157 nsCOMPtr
<nsITimer
> mTimer
;
158 nsAutoArrayPtr
<char> mChunk
;
163 nsInt64 mCurrentSize
;
165 PRInt32 mNonPartialCount
;
167 PRPackedBool mIsPending
;
168 PRPackedBool mDidOnStartRequest
;
171 nsIncrementalDownload::nsIncrementalDownload()
173 , mChunkSize(DEFAULT_CHUNK_SIZE
)
174 , mInterval(DEFAULT_INTERVAL
)
177 , mLoadFlags(LOAD_NORMAL
)
178 , mNonPartialCount(0)
180 , mIsPending(PR_FALSE
)
181 , mDidOnStartRequest(PR_FALSE
)
186 nsIncrementalDownload::FlushChunk()
188 NS_ASSERTION(mTotalSize
!= nsInt64(-1), "total size should be known");
193 nsresult rv
= AppendToFile(mDest
, mChunk
, mChunkLen
);
197 mCurrentSize
+= nsInt64(mChunkLen
);
201 mProgressSink
->OnProgress(this, mObserverContext
,
202 PRUint64(PRInt64(mCurrentSize
)),
203 PRUint64(PRInt64(mTotalSize
)));
208 nsIncrementalDownload::CallOnStartRequest()
210 if (!mObserver
|| mDidOnStartRequest
)
213 mDidOnStartRequest
= PR_TRUE
;
214 return mObserver
->OnStartRequest(this, mObserverContext
);
218 nsIncrementalDownload::CallOnStopRequest()
223 // Ensure that OnStartRequest is always called once before OnStopRequest.
224 nsresult rv
= CallOnStartRequest();
225 if (NS_SUCCEEDED(mStatus
))
228 mIsPending
= PR_FALSE
;
230 mObserver
->OnStopRequest(this, mObserverContext
, mStatus
);
232 mObserverContext
= nsnull
;
236 nsIncrementalDownload::StartTimer(PRInt32 interval
)
239 mTimer
= do_CreateInstance(NS_TIMER_CONTRACTID
, &rv
);
243 return mTimer
->Init(this, interval
* 1000, nsITimer::TYPE_ONE_SHOT
);
247 nsIncrementalDownload::ProcessTimeout()
249 NS_ASSERTION(!mChannel
, "how can we have a channel?");
251 // Handle existing error conditions
252 if (NS_FAILED(mStatus
)) {
259 nsCOMPtr
<nsIChannel
> channel
;
260 nsresult rv
= NS_NewChannel(getter_AddRefs(channel
), mFinalURI
, nsnull
,
261 nsnull
, this, mLoadFlags
);
265 nsCOMPtr
<nsIHttpChannel
> http
= do_QueryInterface(channel
, &rv
);
269 NS_ASSERTION(mCurrentSize
!= nsInt64(-1),
270 "we should know the current file size by now");
272 rv
= ClearRequestHeader(http
);
276 // Don't bother making a range request if we are just going to fetch the
278 if (mInterval
|| mCurrentSize
!= nsInt64(0)) {
280 MakeRangeSpec(mCurrentSize
, mTotalSize
, mChunkSize
, mInterval
== 0, range
);
282 rv
= http
->SetRequestHeader(NS_LITERAL_CSTRING("Range"), range
, PR_FALSE
);
287 rv
= channel
->AsyncOpen(this, nsnull
);
291 // Wait to assign mChannel when we know we are going to succeed. This is
292 // important because we don't want to introduce a reference cycle between
293 // mChannel and this until we know for a fact that AsyncOpen has succeeded,
294 // thus ensuring that our stream listener methods will be invoked.
299 // Reads the current file size and validates it.
301 nsIncrementalDownload::ReadCurrentSize()
304 nsresult rv
= mDest
->GetFileSize((PRInt64
*) &size
);
305 if (rv
== NS_ERROR_FILE_NOT_FOUND
||
306 rv
== NS_ERROR_FILE_TARGET_DOES_NOT_EXIST
) {
319 NS_IMPL_ISUPPORTS8(nsIncrementalDownload
,
320 nsIIncrementalDownload
,
325 nsIInterfaceRequestor
,
327 nsISupportsWeakReference
)
332 nsIncrementalDownload::GetName(nsACString
&name
)
334 NS_ENSURE_TRUE(mURI
, NS_ERROR_NOT_INITIALIZED
);
336 return mURI
->GetSpec(name
);
340 nsIncrementalDownload::IsPending(PRBool
*isPending
)
342 *isPending
= mIsPending
;
347 nsIncrementalDownload::GetStatus(nsresult
*status
)
354 nsIncrementalDownload::Cancel(nsresult status
)
356 NS_ENSURE_ARG(NS_FAILED(status
));
358 // Ignore this cancelation if we're already canceled.
359 if (NS_FAILED(mStatus
))
364 // Nothing more to do if callbacks aren't pending.
369 mChannel
->Cancel(mStatus
);
370 NS_ASSERTION(!mTimer
, "what is this timer object doing here?");
373 // dispatch a timer callback event to drive invoking our listener's
384 nsIncrementalDownload::Suspend()
386 return NS_ERROR_NOT_IMPLEMENTED
;
390 nsIncrementalDownload::Resume()
392 return NS_ERROR_NOT_IMPLEMENTED
;
396 nsIncrementalDownload::GetLoadFlags(nsLoadFlags
*loadFlags
)
398 *loadFlags
= mLoadFlags
;
403 nsIncrementalDownload::SetLoadFlags(nsLoadFlags loadFlags
)
405 mLoadFlags
= loadFlags
;
410 nsIncrementalDownload::GetLoadGroup(nsILoadGroup
**loadGroup
)
412 return NS_ERROR_NOT_IMPLEMENTED
;
416 nsIncrementalDownload::SetLoadGroup(nsILoadGroup
*loadGroup
)
418 return NS_ERROR_NOT_IMPLEMENTED
;
421 // nsIIncrementalDownload
424 nsIncrementalDownload::Init(nsIURI
*uri
, nsIFile
*dest
,
425 PRInt32 chunkSize
, PRInt32 interval
)
427 // Keep it simple: only allow initialization once
428 NS_ENSURE_FALSE(mURI
, NS_ERROR_ALREADY_INITIALIZED
);
430 mDest
= do_QueryInterface(dest
);
431 NS_ENSURE_ARG(mDest
);
437 mChunkSize
= chunkSize
;
439 mInterval
= interval
;
444 nsIncrementalDownload::GetURI(nsIURI
**result
)
446 NS_IF_ADDREF(*result
= mURI
);
451 nsIncrementalDownload::GetFinalURI(nsIURI
**result
)
453 NS_IF_ADDREF(*result
= mFinalURI
);
458 nsIncrementalDownload::GetDestination(nsIFile
**result
)
464 // Return a clone of mDest so that callers may modify the resulting nsIFile
465 // without corrupting our internal object. This also works around the fact
466 // that some nsIFile impls may cache the result of stat'ing the filesystem.
467 return mDest
->Clone(result
);
471 nsIncrementalDownload::GetTotalSize(PRInt64
*result
)
473 *result
= mTotalSize
;
478 nsIncrementalDownload::GetCurrentSize(PRInt64
*result
)
480 *result
= mCurrentSize
;
485 nsIncrementalDownload::Start(nsIRequestObserver
*observer
,
486 nsISupports
*context
)
488 NS_ENSURE_ARG(observer
);
489 NS_ENSURE_FALSE(mIsPending
, NS_ERROR_IN_PROGRESS
);
491 // Observe system shutdown so we can be sure to release any reference held
492 // between ourselves and the timer. We have the observer service hold a weak
493 // reference to us, so that we don't have to worry about calling
494 // RemoveObserver. XXX(darin): The timer code should do this for us.
495 nsCOMPtr
<nsIObserverService
> obs
=
496 do_GetService("@mozilla.org/observer-service;1");
498 obs
->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID
, PR_TRUE
);
500 nsresult rv
= ReadCurrentSize();
508 mObserver
= observer
;
509 mObserverContext
= context
;
510 mProgressSink
= do_QueryInterface(observer
); // ok if null
512 mIsPending
= PR_TRUE
;
516 // nsIRequestObserver
519 nsIncrementalDownload::OnStartRequest(nsIRequest
*request
,
520 nsISupports
*context
)
524 nsCOMPtr
<nsIHttpChannel
> http
= do_QueryInterface(request
, &rv
);
528 // Ensure that we are receiving a 206 response.
530 rv
= http
->GetResponseStatus(&code
);
534 // We may already have the entire file downloaded, in which case
535 // our request for a range beyond the end of the file would have
536 // been met with an error response code.
537 if (code
== 416 && mTotalSize
== nsInt64(-1)) {
538 mTotalSize
= mCurrentSize
;
539 // Return an error code here to suppress OnDataAvailable.
540 return NS_ERROR_DOWNLOAD_COMPLETE
;
542 // The server may have decided to give us all of the data in one chunk. If
543 // we requested a partial range, then we don't want to download all of the
544 // data at once. So, we'll just try again, but if this keeps happening then
545 // we'll eventually give up.
549 if (++mNonPartialCount
> MAX_RETRY_COUNT
) {
550 NS_WARNING("unable to fetch a byte range; giving up");
551 return NS_ERROR_FAILURE
;
553 // Increase delay with each failure.
554 StartTimer(mInterval
* mNonPartialCount
);
555 return NS_ERROR_DOWNLOAD_NOT_PARTIAL
;
557 // Since we have been asked to download the rest of the file, we can deal
558 // with a 200 response. This may result in downloading the beginning of
559 // the file again, but that can't really be helped.
561 NS_WARNING("server response was unexpected");
562 return NS_ERROR_UNEXPECTED
;
565 // We got a partial response, so clear this counter in case the next chunk
566 // results in a 200 response.
567 mNonPartialCount
= 0;
570 // Do special processing after the first response.
571 if (mTotalSize
== nsInt64(-1)) {
572 // Update knowledge of mFinalURI
573 rv
= http
->GetURI(getter_AddRefs(mFinalURI
));
578 // OK, read the Content-Range header to determine the total size of this
581 rv
= http
->GetResponseHeader(NS_LITERAL_CSTRING("Content-Range"), buf
);
584 PRInt32 slash
= buf
.FindChar('/');
585 if (slash
== kNotFound
) {
586 NS_WARNING("server returned invalid Content-Range header!");
587 return NS_ERROR_UNEXPECTED
;
589 if (PR_sscanf(buf
.get() + slash
+ 1, "%lld", (PRInt64
*) &mTotalSize
) != 1)
590 return NS_ERROR_UNEXPECTED
;
592 // Use nsIPropertyBag2 to fetch the content length as it exposes the
593 // value as a 64-bit number.
594 nsCOMPtr
<nsIPropertyBag2
> props
= do_QueryInterface(request
, &rv
);
597 rv
= props
->GetPropertyAsInt64(NS_CHANNEL_PROP_CONTENT_LENGTH
,
599 // We need to know the total size of the thing we're trying to download.
600 if (mTotalSize
== nsInt64(-1)) {
601 NS_WARNING("server returned no content-length header!");
602 return NS_ERROR_UNEXPECTED
;
604 // Need to truncate (or create, if it doesn't exist) the file since we
605 // are downloading the whole thing.
606 WriteToFile(mDest
, nsnull
, 0, PR_WRONLY
| PR_CREATE_FILE
| PR_TRUNCATE
);
610 // Notify observer that we are starting...
611 rv
= CallOnStartRequest();
616 // Adjust mChunkSize accordingly if mCurrentSize is close to mTotalSize.
617 nsInt64 diff
= mTotalSize
- mCurrentSize
;
618 if (diff
<= nsInt64(0)) {
619 NS_WARNING("about to set a bogus chunk size; giving up");
620 return NS_ERROR_UNEXPECTED
;
623 if (diff
< nsInt64(mChunkSize
))
624 mChunkSize
= PRUint32(diff
);
626 mChunk
= new char[mChunkSize
];
628 rv
= NS_ERROR_OUT_OF_MEMORY
;
634 nsIncrementalDownload::OnStopRequest(nsIRequest
*request
,
635 nsISupports
*context
,
638 // Not a real error; just a trick to kill off the channel without our
639 // listener having to care.
640 if (status
== NS_ERROR_DOWNLOAD_NOT_PARTIAL
)
643 // Not a real error; just a trick used to suppress OnDataAvailable calls.
644 if (status
== NS_ERROR_DOWNLOAD_COMPLETE
)
647 if (NS_SUCCEEDED(mStatus
))
651 if (NS_SUCCEEDED(mStatus
))
652 mStatus
= FlushChunk();
654 mChunk
= nsnull
; // deletes memory
660 // Notify listener if we hit an error or finished
661 if (NS_FAILED(mStatus
) || mCurrentSize
== mTotalSize
) {
666 return StartTimer(mInterval
); // Do next chunk
672 nsIncrementalDownload::OnDataAvailable(nsIRequest
*request
,
673 nsISupports
*context
,
674 nsIInputStream
*input
,
679 PRUint32 space
= mChunkSize
- mChunkLen
;
680 PRUint32 n
, len
= PR_MIN(space
, count
);
682 nsresult rv
= input
->Read(mChunk
+ mChunkLen
, len
, &n
);
686 return NS_ERROR_UNEXPECTED
;
691 if (mChunkLen
== mChunkSize
)
701 nsIncrementalDownload::Observe(nsISupports
*subject
, const char *topic
,
702 const PRUnichar
*data
)
704 if (strcmp(topic
, NS_XPCOM_SHUTDOWN_OBSERVER_ID
) == 0) {
705 Cancel(NS_ERROR_ABORT
);
707 // Since the app is shutting down, we need to go ahead and notify our
708 // observer here. Otherwise, we would notify them after XPCOM has been
709 // shutdown or not at all.
712 else if (strcmp(topic
, NS_TIMER_CALLBACK_TOPIC
) == 0) {
714 nsresult rv
= ProcessTimeout();
721 // nsIInterfaceRequestor
724 nsIncrementalDownload::GetInterface(const nsIID
&iid
, void **result
)
726 if (iid
.Equals(NS_GET_IID(nsIChannelEventSink
))) {
728 *result
= static_cast<nsIChannelEventSink
*>(this);
732 nsCOMPtr
<nsIInterfaceRequestor
> ir
= do_QueryInterface(mObserver
);
734 return ir
->GetInterface(iid
, result
);
736 return NS_ERROR_NO_INTERFACE
;
740 nsIncrementalDownload::ClearRequestHeader(nsIHttpChannel
*channel
)
742 NS_ENSURE_ARG(channel
);
744 // We don't support encodings -- they make the Content-Length not equal
745 // to the actual size of the data.
746 return channel
->SetRequestHeader(NS_LITERAL_CSTRING("Accept-Encoding"),
747 NS_LITERAL_CSTRING(""), PR_FALSE
);
750 // nsIChannelEventSink
753 nsIncrementalDownload::OnChannelRedirect(nsIChannel
*oldChannel
,
754 nsIChannel
*newChannel
,
757 // In response to a redirect, we need to propagate the Range header. See bug
758 // 311595. Any failure code returned from this function aborts the redirect.
760 nsCOMPtr
<nsIHttpChannel
> http
= do_QueryInterface(oldChannel
);
761 NS_ENSURE_STATE(http
);
763 nsCOMPtr
<nsIHttpChannel
> newHttpChannel
= do_QueryInterface(newChannel
);
764 NS_ENSURE_STATE(newHttpChannel
);
766 NS_NAMED_LITERAL_CSTRING(rangeHdr
, "Range");
768 nsresult rv
= ClearRequestHeader(newHttpChannel
);
772 // If we didn't have a Range header, then we must be doing a full download.
773 nsCAutoString rangeVal
;
774 http
->GetRequestHeader(rangeHdr
, rangeVal
);
775 if (!rangeVal
.IsEmpty()) {
776 rv
= newHttpChannel
->SetRequestHeader(rangeHdr
, rangeVal
, PR_FALSE
);
777 NS_ENSURE_SUCCESS(rv
, rv
);
780 // Give the observer a chance to see this redirect notification.
781 nsCOMPtr
<nsIChannelEventSink
> sink
= do_GetInterface(mObserver
);
783 rv
= sink
->OnChannelRedirect(oldChannel
, newChannel
, flags
);
785 // Update mChannel, so we can Cancel the new channel.
786 if (NS_SUCCEEDED(rv
))
787 mChannel
= newChannel
;
793 net_NewIncrementalDownload(nsISupports
*outer
, const nsIID
&iid
, void **result
)
796 return NS_ERROR_NO_AGGREGATION
;
798 nsIncrementalDownload
*d
= new nsIncrementalDownload();
800 return NS_ERROR_OUT_OF_MEMORY
;
803 nsresult rv
= d
->QueryInterface(iid
, result
);