mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-01-05 15:59:45 +00:00
Bug 887425 - BackgroundFileSaver should support appending to an existing file. r=mcmanus, sr=biesi
This commit is contained in:
parent
b81ea1b6ba
commit
9c4fd821b1
@ -38,7 +38,7 @@ interface nsIFile;
|
||||
* public methods of the interface may only be called from the main
|
||||
* thread.
|
||||
*/
|
||||
[scriptable, uuid(17a2ff32-918f-11e2-8fc9-f9626188709b)]
|
||||
[scriptable, uuid(581a99ca-dc8d-4cee-ac95-99156e7517ed)]
|
||||
interface nsIBackgroundFileSaver : nsISupports
|
||||
{
|
||||
/**
|
||||
@ -52,15 +52,8 @@ interface nsIBackgroundFileSaver : nsISupports
|
||||
attribute nsIBackgroundFileSaverObserver observer;
|
||||
|
||||
/**
|
||||
* The SHA256 hash in raw bytes associated with the file that was downloaded.
|
||||
*
|
||||
* @remarks Reading this will throw NS_ERROR_NOT_AVAILABLE unless
|
||||
* sha256enabled is true and onSaveComplete has been called.
|
||||
*/
|
||||
readonly attribute ACString sha256Hash;
|
||||
|
||||
/**
|
||||
* Compute SHA256.
|
||||
* Instructs the component to compute the SHA-256 hash of the target file, and
|
||||
* make it available in the sha256Hash property.
|
||||
*
|
||||
* @remarks This must be set on the main thread before the first call to
|
||||
* setTarget.
|
||||
@ -68,10 +61,38 @@ interface nsIBackgroundFileSaver : nsISupports
|
||||
void enableSha256();
|
||||
|
||||
/**
|
||||
* Sets the name of the output file to be written. The output file may
|
||||
* already exist, in which case it will be overwritten. The target can be
|
||||
* changed after data has already been fed, in which case the existing file
|
||||
* will be moved to the new destination.
|
||||
* The SHA-256 hash, in raw bytes, associated with the data that was saved.
|
||||
*
|
||||
* In case the enableAppend method has been called, the hash computation
|
||||
* includes the contents of the existing file, if any.
|
||||
*
|
||||
* @throws NS_ERROR_NOT_AVAILABLE
|
||||
* In case the enableSha256 method has not been called, or before the
|
||||
* onSaveComplete method has been called to notify success.
|
||||
*/
|
||||
readonly attribute ACString sha256Hash;
|
||||
|
||||
/**
|
||||
* Instructs the component to append data to the initial target file, that
|
||||
* will be specified by the first call to the setTarget method, instead of
|
||||
* overwriting the file.
|
||||
*
|
||||
* If the initial target file does not exist, this method has no effect.
|
||||
*
|
||||
* @remarks This must be set on the main thread before the first call to
|
||||
* setTarget.
|
||||
*/
|
||||
void enableAppend();
|
||||
|
||||
/**
|
||||
* Sets the name of the output file to be written. The target can be changed
|
||||
* after data has already been fed, in which case the existing file will be
|
||||
* moved to the new destination.
|
||||
*
|
||||
* In case the specified file already exists, and this method is called for
|
||||
* the first time, the file may be either overwritten or appended to, based on
|
||||
* whether the enableAppend method was called. Subsequent calls always
|
||||
* overwrite the specified target file with the previously saved data.
|
||||
*
|
||||
* No file will be written until this function is called at least once. It's
|
||||
* recommended not to feed any data until the output file is set.
|
||||
@ -132,7 +153,7 @@ interface nsIBackgroundFileSaverObserver : nsISupports
|
||||
* @param aSaver
|
||||
* Reference to the object that raised the notification.
|
||||
* @param aStatus
|
||||
* Result code that determines whether the operation succeded or
|
||||
* Result code that determines whether the operation succeeded or
|
||||
* failed, as well as the failure reason.
|
||||
*/
|
||||
void onSaveComplete(in nsIBackgroundFileSaver aSaver, in nsresult aStatus);
|
||||
|
@ -25,9 +25,9 @@ namespace net {
|
||||
//// Globals
|
||||
|
||||
/**
|
||||
* Buffer size for writing to the output file.
|
||||
* Buffer size for writing to the output file or reading from the input file.
|
||||
*/
|
||||
#define BUFFERED_OUTPUT_SIZE (1024 * 32)
|
||||
#define BUFFERED_IO_SIZE (1024 * 32)
|
||||
|
||||
/**
|
||||
* When this upper limit is reached, the original request is suspended.
|
||||
@ -82,6 +82,7 @@ BackgroundFileSaver::BackgroundFileSaver()
|
||||
, mFinishRequested(false)
|
||||
, mComplete(false)
|
||||
, mStatus(NS_OK)
|
||||
, mAppend(false)
|
||||
, mAssignedTarget(nullptr)
|
||||
, mAssignedTargetKeepPartial(false)
|
||||
, mAsyncCopyContext(nullptr)
|
||||
@ -162,6 +163,18 @@ BackgroundFileSaver::SetObserver(nsIBackgroundFileSaverObserver *aObserver)
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// Called on the control thread.
|
||||
NS_IMETHODIMP
|
||||
BackgroundFileSaver::EnableAppend()
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread");
|
||||
|
||||
MutexAutoLock lock(mLock);
|
||||
mAppend = true;
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// Called on the control thread.
|
||||
NS_IMETHODIMP
|
||||
BackgroundFileSaver::SetTarget(nsIFile *aTarget, bool aKeepPartial)
|
||||
@ -364,16 +377,17 @@ BackgroundFileSaver::ProcessStateChange()
|
||||
nsCOMPtr<nsIFile> target;
|
||||
bool targetKeepPartial;
|
||||
bool sha256Enabled = false;
|
||||
bool append = false;
|
||||
{
|
||||
MutexAutoLock lock(mLock);
|
||||
|
||||
target = mAssignedTarget;
|
||||
targetKeepPartial = mAssignedTargetKeepPartial;
|
||||
sha256Enabled = mSha256Enabled;
|
||||
append = mAppend;
|
||||
|
||||
// From now on, another attention event needs to be posted if state changes.
|
||||
mWorkerThreadAttentionRequested = false;
|
||||
|
||||
sha256Enabled = mSha256Enabled;
|
||||
}
|
||||
|
||||
// The target can only be null if it has never been assigned. In this case,
|
||||
@ -382,13 +396,20 @@ BackgroundFileSaver::ProcessStateChange()
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// We will append to the target file only if we already started writing it.
|
||||
bool equalToCurrent = false;
|
||||
int32_t creationIoFlags = PR_CREATE_FILE | PR_TRUNCATE;
|
||||
if (mActualTarget) {
|
||||
creationIoFlags = PR_APPEND;
|
||||
bool isContinuation = !!mActualTarget;
|
||||
|
||||
// Verify whether we have actually been instructed to use a different file.
|
||||
// We will append to the initial target file only if it was requested by the
|
||||
// caller, but we'll always append on subsequent accesses to the target file.
|
||||
int32_t creationIoFlags;
|
||||
if (isContinuation) {
|
||||
creationIoFlags = PR_APPEND;
|
||||
} else {
|
||||
creationIoFlags = (append ? PR_APPEND : PR_TRUNCATE) | PR_CREATE_FILE;
|
||||
}
|
||||
|
||||
// Verify whether we have actually been instructed to use a different file.
|
||||
bool equalToCurrent = false;
|
||||
if (isContinuation) {
|
||||
rv = mActualTarget->Equals(target, &equalToCurrent);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
if (!equalToCurrent)
|
||||
@ -440,18 +461,20 @@ BackgroundFileSaver::ProcessStateChange()
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
}
|
||||
|
||||
// The pending rename operation might be the last task before finishing.
|
||||
if (CheckCompletion()) {
|
||||
return NS_OK;
|
||||
}
|
||||
if (isContinuation) {
|
||||
// The pending rename operation might be the last task before finishing. We
|
||||
// may return here only if we have already created the target file.
|
||||
if (CheckCompletion()) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// Even if the operation did not complete, the pipe input stream may be empty
|
||||
// and may have been closed already. We detect this case using the Available
|
||||
// property, because it never returns an error if there is more data to be
|
||||
// consumed. If the pipe input stream is closed, we just exit and wait for
|
||||
// more calls like SetTarget or Finish to be invoked on the control thread.
|
||||
// However, we still truncate the file if we are expected to do that.
|
||||
if (creationIoFlags == PR_APPEND) {
|
||||
// Even if the operation did not complete, the pipe input stream may be
|
||||
// empty and may have been closed already. We detect this case using the
|
||||
// Available property, because it never returns an error if there is more
|
||||
// data to be consumed. If the pipe input stream is closed, we just exit
|
||||
// and wait for more calls like SetTarget or Finish to be invoked on the
|
||||
// control thread. However, we still truncate the file or create the
|
||||
// initial digest context if we are expected to do that.
|
||||
uint64_t available;
|
||||
rv = mPipeInputStream->Available(&available);
|
||||
if (NS_FAILED(rv)) {
|
||||
@ -459,6 +482,53 @@ BackgroundFileSaver::ProcessStateChange()
|
||||
}
|
||||
}
|
||||
|
||||
// Create the digest context if requested and NSS hasn't been shut down.
|
||||
if (sha256Enabled && !mDigestContext) {
|
||||
nsNSSShutDownPreventionLock lock;
|
||||
if (!isAlreadyShutDown()) {
|
||||
mDigestContext =
|
||||
PK11_CreateDigestContext(static_cast<SECOidTag>(SEC_OID_SHA256));
|
||||
NS_ENSURE_TRUE(mDigestContext, NS_ERROR_OUT_OF_MEMORY);
|
||||
}
|
||||
}
|
||||
|
||||
// When we are requested to append to an existing file, we should read the
|
||||
// existing data and ensure we include it as part of the final hash.
|
||||
if (append && !isContinuation) {
|
||||
nsCOMPtr<nsIInputStream> inputStream;
|
||||
rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream),
|
||||
mActualTarget,
|
||||
PR_RDONLY | nsIFile::OS_READAHEAD);
|
||||
if (rv != NS_ERROR_FILE_NOT_FOUND) {
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
char buffer[BUFFERED_IO_SIZE];
|
||||
while (true) {
|
||||
uint32_t count;
|
||||
rv = inputStream->Read(buffer, BUFFERED_IO_SIZE, &count);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
if (count == 0) {
|
||||
// We reached the end of the file.
|
||||
break;
|
||||
}
|
||||
|
||||
nsNSSShutDownPreventionLock lock;
|
||||
if (isAlreadyShutDown()) {
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
nsresult rv = MapSECStatus(PK11_DigestOp(mDigestContext,
|
||||
uint8_t_ptr_cast(buffer),
|
||||
count));
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
}
|
||||
|
||||
rv = inputStream->Close();
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
}
|
||||
}
|
||||
|
||||
// Create the target file, or append to it if we already started writing it.
|
||||
nsCOMPtr<nsIOutputStream> outputStream;
|
||||
rv = NS_NewLocalFileOutputStream(getter_AddRefs(outputStream),
|
||||
@ -466,24 +536,12 @@ BackgroundFileSaver::ProcessStateChange()
|
||||
PR_WRONLY | creationIoFlags, 0600);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
outputStream = NS_BufferOutputStream(outputStream, BUFFERED_OUTPUT_SIZE);
|
||||
outputStream = NS_BufferOutputStream(outputStream, BUFFERED_IO_SIZE);
|
||||
if (!outputStream) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
// Wrap the output stream in a hashing stream if hashing is enabled and NSS
|
||||
// hasn't been shut down.
|
||||
bool isShutDown = false;
|
||||
if (sha256Enabled && !mDigestContext) {
|
||||
nsNSSShutDownPreventionLock lock;
|
||||
if (!(isShutDown = isAlreadyShutDown())) {
|
||||
mDigestContext =
|
||||
PK11_CreateDigestContext(static_cast<SECOidTag>(SEC_OID_SHA256));
|
||||
NS_ENSURE_TRUE(mDigestContext, NS_ERROR_OUT_OF_MEMORY);
|
||||
}
|
||||
}
|
||||
MOZ_ASSERT(!sha256Enabled || mDigestContext || isShutDown,
|
||||
"Hashing enabled but creating digest context didn't work");
|
||||
// Wrap the output stream so that it feeds the digest context if needed.
|
||||
if (mDigestContext) {
|
||||
// No need to acquire the NSS lock here, DigestOutputStream must acquire it
|
||||
// in any case before each asynchronous write. Constructing the
|
||||
@ -511,7 +569,7 @@ BackgroundFileSaver::ProcessStateChange()
|
||||
}
|
||||
}
|
||||
|
||||
// If the operation succeded, we must ensure that we keep this object alive
|
||||
// If the operation succeeded, we must ensure that we keep this object alive
|
||||
// for the entire duration of the copy, since only the raw pointer will be
|
||||
// provided as the argument of the AsyncCopyCallback function. We can add the
|
||||
// reference now, after NS_AsyncCopy returned, because it always starts
|
||||
|
@ -154,6 +154,12 @@ private:
|
||||
*/
|
||||
nsresult mStatus;
|
||||
|
||||
/**
|
||||
* True if we should append data to the initial target file, instead of
|
||||
* overwriting it.
|
||||
*/
|
||||
bool mAppend;
|
||||
|
||||
/**
|
||||
* Set by the control thread to the target file name that will be used by the
|
||||
* worker thread, as soon as it is possible to update mActualTarget and open
|
||||
|
@ -45,13 +45,17 @@ const TEST_FILE_NAME_1 = "test-backgroundfilesaver-1.txt";
|
||||
const TEST_FILE_NAME_2 = "test-backgroundfilesaver-2.txt";
|
||||
const TEST_FILE_NAME_3 = "test-backgroundfilesaver-3.txt";
|
||||
|
||||
// A map of test data length to the expected hash
|
||||
// A map of test data length to the expected SHA-256 hashes
|
||||
const EXPECTED_HASHES = {
|
||||
// SHA-256 hash of TEST_DATA_SHORT
|
||||
// No data
|
||||
0 : "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
// TEST_DATA_SHORT
|
||||
40 : "f37176b690e8744ee990a206c086cba54d1502aa2456c3b0c84ef6345d72a192",
|
||||
// SHA-256 hash of TEST_DATA_SHORT + TEST_DATA_SHORT
|
||||
// TEST_DATA_SHORT + TEST_DATA_SHORT
|
||||
80 : "780c0e91f50bb7ec922cc11e16859e6d5df283c0d9470f61772e3d79f41eeb58",
|
||||
// SHA-256 hash of a bunch of dashes
|
||||
// TEST_DATA_LONG
|
||||
8388608 : "e3611a47714c42bdf326acfb2eb6ed9fa4cca65cb7d7be55217770a5bf5e7ff0",
|
||||
// TEST_DATA_LONG + TEST_DATA_LONG
|
||||
16777216 : "03a0db69a30140f307587ee746a539247c181bafd85b85c8516a3533c7d9ea1d"
|
||||
};
|
||||
|
||||
@ -408,18 +412,18 @@ add_task(function test_setTarget_after_close_stream()
|
||||
let saver = new BackgroundFileSaverOutputStream();
|
||||
saver.enableSha256();
|
||||
let completionPromise = promiseSaverComplete(saver);
|
||||
|
||||
|
||||
// Copy some data to the output stream of the file saver. This data must
|
||||
// be shorter than the internal component's pipe buffer for the test to
|
||||
// succeed, because otherwise the test would block waiting for the write to
|
||||
// complete.
|
||||
yield promiseCopyToSaver(TEST_DATA_SHORT, saver, true);
|
||||
|
||||
|
||||
// Set the target file and wait for the output to finish.
|
||||
saver.setTarget(destFile, false);
|
||||
saver.finish(Cr.NS_OK);
|
||||
yield completionPromise;
|
||||
|
||||
|
||||
// Verify results.
|
||||
yield promiseVerifyContents(destFile, TEST_DATA_SHORT);
|
||||
do_check_eq(EXPECTED_HASHES[TEST_DATA_SHORT.length],
|
||||
@ -455,6 +459,38 @@ add_task(function test_setTarget_multiple()
|
||||
destFile.remove(false);
|
||||
});
|
||||
|
||||
add_task(function test_enableAppend_hash()
|
||||
{
|
||||
// This test checks append mode, also verifying that the computed hash
|
||||
// includes the contents of the existing data.
|
||||
let destFile = getTempFile(TEST_FILE_NAME_1);
|
||||
|
||||
// Test the case where the file does not already exists first, then the case
|
||||
// where the file already exists.
|
||||
for (let i = 0; i < 2; i++) {
|
||||
let saver = new BackgroundFileSaverOutputStream();
|
||||
saver.enableAppend();
|
||||
saver.enableSha256();
|
||||
let completionPromise = promiseSaverComplete(saver);
|
||||
|
||||
saver.setTarget(destFile, false);
|
||||
yield promiseCopyToSaver(TEST_DATA_LONG, saver, true);
|
||||
|
||||
saver.finish(Cr.NS_OK);
|
||||
yield completionPromise;
|
||||
|
||||
// Verify results.
|
||||
let expectedContents = (i == 0 ? TEST_DATA_LONG
|
||||
: TEST_DATA_LONG + TEST_DATA_LONG);
|
||||
yield promiseVerifyContents(destFile, expectedContents);
|
||||
do_check_eq(EXPECTED_HASHES[expectedContents.length],
|
||||
toHex(saver.sha256Hash));
|
||||
}
|
||||
|
||||
// Clean up.
|
||||
destFile.remove(false);
|
||||
});
|
||||
|
||||
add_task(function test_finish_only()
|
||||
{
|
||||
// This test checks creating the object and doing nothing.
|
||||
@ -468,6 +504,57 @@ add_task(function test_finish_only()
|
||||
yield completionPromise;
|
||||
});
|
||||
|
||||
add_task(function test_empty()
|
||||
{
|
||||
// This test checks we still create an empty file when no data is fed.
|
||||
let destFile = getTempFile(TEST_FILE_NAME_1);
|
||||
|
||||
let saver = new BackgroundFileSaverOutputStream();
|
||||
let completionPromise = promiseSaverComplete(saver);
|
||||
|
||||
saver.setTarget(destFile, false);
|
||||
yield promiseCopyToSaver("", saver, true);
|
||||
|
||||
saver.finish(Cr.NS_OK);
|
||||
yield completionPromise;
|
||||
|
||||
// Verify results.
|
||||
do_check_true(destFile.exists());
|
||||
do_check_eq(destFile.fileSize, 0);
|
||||
|
||||
// Clean up.
|
||||
destFile.remove(false);
|
||||
});
|
||||
|
||||
add_task(function test_empty_hash()
|
||||
{
|
||||
// This test checks the hash of an empty file, both in normal and append mode.
|
||||
let destFile = getTempFile(TEST_FILE_NAME_1);
|
||||
|
||||
// Test normal mode first, then append mode.
|
||||
for (let i = 0; i < 2; i++) {
|
||||
let saver = new BackgroundFileSaverOutputStream();
|
||||
if (i == 1) {
|
||||
saver.enableAppend();
|
||||
}
|
||||
saver.enableSha256();
|
||||
let completionPromise = promiseSaverComplete(saver);
|
||||
|
||||
saver.setTarget(destFile, false);
|
||||
yield promiseCopyToSaver("", saver, true);
|
||||
|
||||
saver.finish(Cr.NS_OK);
|
||||
yield completionPromise;
|
||||
|
||||
// Verify results.
|
||||
do_check_eq(destFile.fileSize, 0);
|
||||
do_check_eq(EXPECTED_HASHES[0], toHex(saver.sha256Hash));
|
||||
}
|
||||
|
||||
// Clean up.
|
||||
destFile.remove(false);
|
||||
});
|
||||
|
||||
add_task(function test_invalid_hash()
|
||||
{
|
||||
let saver = new BackgroundFileSaverStreamListener();
|
||||
|
Loading…
Reference in New Issue
Block a user