1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include "webkit/browser/fileapi/file_system_context.h" 6 7 #include "base/bind.h" 8 #include "base/single_thread_task_runner.h" 9 #include "base/stl_util.h" 10 #include "base/task_runner_util.h" 11 #include "net/url_request/url_request.h" 12 #include "url/gurl.h" 13 #include "webkit/browser/blob/file_stream_reader.h" 14 #include "webkit/browser/fileapi/copy_or_move_file_validator.h" 15 #include "webkit/browser/fileapi/external_mount_points.h" 16 #include "webkit/browser/fileapi/file_permission_policy.h" 17 #include "webkit/browser/fileapi/file_stream_writer.h" 18 #include "webkit/browser/fileapi/file_system_file_util.h" 19 #include "webkit/browser/fileapi/file_system_operation.h" 20 #include "webkit/browser/fileapi/file_system_operation_runner.h" 21 #include "webkit/browser/fileapi/file_system_options.h" 22 #include "webkit/browser/fileapi/file_system_quota_client.h" 23 #include "webkit/browser/fileapi/file_system_url.h" 24 #include "webkit/browser/fileapi/isolated_context.h" 25 #include "webkit/browser/fileapi/isolated_file_system_backend.h" 26 #include "webkit/browser/fileapi/mount_points.h" 27 #include "webkit/browser/fileapi/quota/quota_reservation.h" 28 #include "webkit/browser/fileapi/sandbox_file_system_backend.h" 29 #include "webkit/browser/quota/quota_manager_proxy.h" 30 #include "webkit/browser/quota/special_storage_policy.h" 31 #include "webkit/common/fileapi/file_system_info.h" 32 #include "webkit/common/fileapi/file_system_util.h" 33 34 using quota::QuotaClient; 35 36 namespace fileapi { 37 38 namespace { 39 40 QuotaClient* CreateQuotaClient( 41 FileSystemContext* context, 42 bool is_incognito) { 43 return new FileSystemQuotaClient(context, is_incognito); 44 } 45 46 47 void DidGetMetadataForResolveURL( 48 const base::FilePath& path, 49 const FileSystemContext::ResolveURLCallback& callback, 50 const FileSystemInfo& info, 51 base::File::Error error, 52 const base::File::Info& file_info) { 53 if (error != base::File::FILE_OK) { 54 if (error == base::File::FILE_ERROR_NOT_FOUND) { 55 callback.Run(base::File::FILE_OK, info, path, 56 FileSystemContext::RESOLVED_ENTRY_NOT_FOUND); 57 } else { 58 callback.Run(error, FileSystemInfo(), base::FilePath(), 59 FileSystemContext::RESOLVED_ENTRY_NOT_FOUND); 60 } 61 return; 62 } 63 callback.Run(error, info, path, file_info.is_directory ? 64 FileSystemContext::RESOLVED_ENTRY_DIRECTORY : 65 FileSystemContext::RESOLVED_ENTRY_FILE); 66 } 67 68 void RelayResolveURLCallback( 69 scoped_refptr<base::MessageLoopProxy> message_loop, 70 const FileSystemContext::ResolveURLCallback& callback, 71 base::File::Error result, 72 const FileSystemInfo& info, 73 const base::FilePath& file_path, 74 FileSystemContext::ResolvedEntryType type) { 75 message_loop->PostTask( 76 FROM_HERE, base::Bind(callback, result, info, file_path, type)); 77 } 78 79 } // namespace 80 81 // static 82 int FileSystemContext::GetPermissionPolicy(FileSystemType type) { 83 switch (type) { 84 case kFileSystemTypeTemporary: 85 case kFileSystemTypePersistent: 86 case kFileSystemTypeSyncable: 87 return FILE_PERMISSION_SANDBOX; 88 89 case kFileSystemTypeDrive: 90 case kFileSystemTypeNativeForPlatformApp: 91 case kFileSystemTypeNativeLocal: 92 case kFileSystemTypeCloudDevice: 93 case kFileSystemTypeProvided: 94 case kFileSystemTypeDeviceMediaAsFileStorage: 95 return FILE_PERMISSION_USE_FILE_PERMISSION; 96 97 case kFileSystemTypeRestrictedNativeLocal: 98 return FILE_PERMISSION_READ_ONLY | 99 FILE_PERMISSION_USE_FILE_PERMISSION; 100 101 case kFileSystemTypeDeviceMedia: 102 case kFileSystemTypeIphoto: 103 case kFileSystemTypeItunes: 104 case kFileSystemTypeNativeMedia: 105 case kFileSystemTypePicasa: 106 return FILE_PERMISSION_USE_FILE_PERMISSION; 107 108 // Following types are only accessed via IsolatedFileSystem, and 109 // don't have their own permission policies. 110 case kFileSystemTypeDragged: 111 case kFileSystemTypeForTransientFile: 112 case kFileSystemTypePluginPrivate: 113 return FILE_PERMISSION_ALWAYS_DENY; 114 115 // Following types only appear as mount_type, and will not be 116 // queried for their permission policies. 117 case kFileSystemTypeIsolated: 118 case kFileSystemTypeExternal: 119 return FILE_PERMISSION_ALWAYS_DENY; 120 121 // Following types should not be used to access files by FileAPI clients. 122 case kFileSystemTypeTest: 123 case kFileSystemTypeSyncableForInternalSync: 124 case kFileSystemInternalTypeEnumEnd: 125 case kFileSystemInternalTypeEnumStart: 126 case kFileSystemTypeUnknown: 127 return FILE_PERMISSION_ALWAYS_DENY; 128 } 129 NOTREACHED(); 130 return FILE_PERMISSION_ALWAYS_DENY; 131 } 132 133 FileSystemContext::FileSystemContext( 134 base::SingleThreadTaskRunner* io_task_runner, 135 base::SequencedTaskRunner* file_task_runner, 136 ExternalMountPoints* external_mount_points, 137 quota::SpecialStoragePolicy* special_storage_policy, 138 quota::QuotaManagerProxy* quota_manager_proxy, 139 ScopedVector<FileSystemBackend> additional_backends, 140 const std::vector<URLRequestAutoMountHandler>& auto_mount_handlers, 141 const base::FilePath& partition_path, 142 const FileSystemOptions& options) 143 : io_task_runner_(io_task_runner), 144 default_file_task_runner_(file_task_runner), 145 quota_manager_proxy_(quota_manager_proxy), 146 sandbox_delegate_(new SandboxFileSystemBackendDelegate( 147 quota_manager_proxy, 148 file_task_runner, 149 partition_path, 150 special_storage_policy, 151 options)), 152 sandbox_backend_(new SandboxFileSystemBackend( 153 sandbox_delegate_.get())), 154 isolated_backend_(new IsolatedFileSystemBackend()), 155 plugin_private_backend_(new PluginPrivateFileSystemBackend( 156 file_task_runner, 157 partition_path, 158 special_storage_policy, 159 options)), 160 additional_backends_(additional_backends.Pass()), 161 auto_mount_handlers_(auto_mount_handlers), 162 external_mount_points_(external_mount_points), 163 partition_path_(partition_path), 164 is_incognito_(options.is_incognito()), 165 operation_runner_(new FileSystemOperationRunner(this)) { 166 RegisterBackend(sandbox_backend_.get()); 167 RegisterBackend(isolated_backend_.get()); 168 RegisterBackend(plugin_private_backend_.get()); 169 170 for (ScopedVector<FileSystemBackend>::const_iterator iter = 171 additional_backends_.begin(); 172 iter != additional_backends_.end(); ++iter) { 173 RegisterBackend(*iter); 174 } 175 176 if (quota_manager_proxy) { 177 // Quota client assumes all backends have registered. 178 quota_manager_proxy->RegisterClient(CreateQuotaClient( 179 this, options.is_incognito())); 180 } 181 182 sandbox_backend_->Initialize(this); 183 isolated_backend_->Initialize(this); 184 plugin_private_backend_->Initialize(this); 185 for (ScopedVector<FileSystemBackend>::const_iterator iter = 186 additional_backends_.begin(); 187 iter != additional_backends_.end(); ++iter) { 188 (*iter)->Initialize(this); 189 } 190 191 // Additional mount points must be added before regular system-wide 192 // mount points. 193 if (external_mount_points) 194 url_crackers_.push_back(external_mount_points); 195 url_crackers_.push_back(ExternalMountPoints::GetSystemInstance()); 196 url_crackers_.push_back(IsolatedContext::GetInstance()); 197 } 198 199 bool FileSystemContext::DeleteDataForOriginOnFileTaskRunner( 200 const GURL& origin_url) { 201 DCHECK(default_file_task_runner()->RunsTasksOnCurrentThread()); 202 DCHECK(origin_url == origin_url.GetOrigin()); 203 204 bool success = true; 205 for (FileSystemBackendMap::iterator iter = backend_map_.begin(); 206 iter != backend_map_.end(); 207 ++iter) { 208 FileSystemBackend* backend = iter->second; 209 if (!backend->GetQuotaUtil()) 210 continue; 211 if (backend->GetQuotaUtil()->DeleteOriginDataOnFileTaskRunner( 212 this, quota_manager_proxy(), origin_url, iter->first) 213 != base::File::FILE_OK) { 214 // Continue the loop, but record the failure. 215 success = false; 216 } 217 } 218 219 return success; 220 } 221 222 scoped_refptr<QuotaReservation> 223 FileSystemContext::CreateQuotaReservationOnFileTaskRunner( 224 const GURL& origin_url, 225 FileSystemType type) { 226 DCHECK(default_file_task_runner()->RunsTasksOnCurrentThread()); 227 FileSystemBackend* backend = GetFileSystemBackend(type); 228 if (!backend || !backend->GetQuotaUtil()) 229 return scoped_refptr<QuotaReservation>(); 230 return backend->GetQuotaUtil()->CreateQuotaReservationOnFileTaskRunner( 231 origin_url, type); 232 } 233 234 void FileSystemContext::Shutdown() { 235 if (!io_task_runner_->RunsTasksOnCurrentThread()) { 236 io_task_runner_->PostTask( 237 FROM_HERE, base::Bind(&FileSystemContext::Shutdown, 238 make_scoped_refptr(this))); 239 return; 240 } 241 operation_runner_->Shutdown(); 242 } 243 244 FileSystemQuotaUtil* 245 FileSystemContext::GetQuotaUtil(FileSystemType type) const { 246 FileSystemBackend* backend = GetFileSystemBackend(type); 247 if (!backend) 248 return NULL; 249 return backend->GetQuotaUtil(); 250 } 251 252 AsyncFileUtil* FileSystemContext::GetAsyncFileUtil( 253 FileSystemType type) const { 254 FileSystemBackend* backend = GetFileSystemBackend(type); 255 if (!backend) 256 return NULL; 257 return backend->GetAsyncFileUtil(type); 258 } 259 260 CopyOrMoveFileValidatorFactory* 261 FileSystemContext::GetCopyOrMoveFileValidatorFactory( 262 FileSystemType type, base::File::Error* error_code) const { 263 DCHECK(error_code); 264 *error_code = base::File::FILE_OK; 265 FileSystemBackend* backend = GetFileSystemBackend(type); 266 if (!backend) 267 return NULL; 268 return backend->GetCopyOrMoveFileValidatorFactory( 269 type, error_code); 270 } 271 272 FileSystemBackend* FileSystemContext::GetFileSystemBackend( 273 FileSystemType type) const { 274 FileSystemBackendMap::const_iterator found = backend_map_.find(type); 275 if (found != backend_map_.end()) 276 return found->second; 277 NOTREACHED() << "Unknown filesystem type: " << type; 278 return NULL; 279 } 280 281 bool FileSystemContext::IsSandboxFileSystem(FileSystemType type) const { 282 FileSystemBackendMap::const_iterator found = backend_map_.find(type); 283 return found != backend_map_.end() && found->second->GetQuotaUtil(); 284 } 285 286 const UpdateObserverList* FileSystemContext::GetUpdateObservers( 287 FileSystemType type) const { 288 FileSystemBackend* backend = GetFileSystemBackend(type); 289 if (backend->GetQuotaUtil()) 290 return backend->GetQuotaUtil()->GetUpdateObservers(type); 291 return NULL; 292 } 293 294 const AccessObserverList* FileSystemContext::GetAccessObservers( 295 FileSystemType type) const { 296 FileSystemBackend* backend = GetFileSystemBackend(type); 297 if (backend->GetQuotaUtil()) 298 return backend->GetQuotaUtil()->GetAccessObservers(type); 299 return NULL; 300 } 301 302 void FileSystemContext::GetFileSystemTypes( 303 std::vector<FileSystemType>* types) const { 304 types->clear(); 305 for (FileSystemBackendMap::const_iterator iter = backend_map_.begin(); 306 iter != backend_map_.end(); ++iter) 307 types->push_back(iter->first); 308 } 309 310 ExternalFileSystemBackend* 311 FileSystemContext::external_backend() const { 312 return static_cast<ExternalFileSystemBackend*>( 313 GetFileSystemBackend(kFileSystemTypeExternal)); 314 } 315 316 void FileSystemContext::OpenFileSystem( 317 const GURL& origin_url, 318 FileSystemType type, 319 OpenFileSystemMode mode, 320 const OpenFileSystemCallback& callback) { 321 DCHECK(io_task_runner_->RunsTasksOnCurrentThread()); 322 DCHECK(!callback.is_null()); 323 324 if (!FileSystemContext::IsSandboxFileSystem(type)) { 325 // Disallow opening a non-sandboxed filesystem. 326 callback.Run(GURL(), std::string(), base::File::FILE_ERROR_SECURITY); 327 return; 328 } 329 330 FileSystemBackend* backend = GetFileSystemBackend(type); 331 if (!backend) { 332 callback.Run(GURL(), std::string(), base::File::FILE_ERROR_SECURITY); 333 return; 334 } 335 336 backend->ResolveURL( 337 CreateCrackedFileSystemURL(origin_url, type, base::FilePath()), 338 mode, 339 callback); 340 } 341 342 void FileSystemContext::ResolveURL( 343 const FileSystemURL& url, 344 const ResolveURLCallback& callback) { 345 DCHECK(!callback.is_null()); 346 347 // If not on IO thread, forward before passing the task to the backend. 348 if (!io_task_runner_->RunsTasksOnCurrentThread()) { 349 ResolveURLCallback relay_callback = 350 base::Bind(&RelayResolveURLCallback, 351 base::MessageLoopProxy::current(), callback); 352 io_task_runner_->PostTask( 353 FROM_HERE, 354 base::Bind(&FileSystemContext::ResolveURL, this, url, relay_callback)); 355 return; 356 } 357 358 FileSystemBackend* backend = GetFileSystemBackend(url.type()); 359 if (!backend) { 360 callback.Run(base::File::FILE_ERROR_SECURITY, 361 FileSystemInfo(), base::FilePath(), 362 FileSystemContext::RESOLVED_ENTRY_NOT_FOUND); 363 return; 364 } 365 366 backend->ResolveURL( 367 url, 368 OPEN_FILE_SYSTEM_FAIL_IF_NONEXISTENT, 369 base::Bind(&FileSystemContext::DidOpenFileSystemForResolveURL, 370 this, 371 url, 372 callback)); 373 } 374 375 void FileSystemContext::AttemptAutoMountForURLRequest( 376 const net::URLRequest* url_request, 377 const std::string& storage_domain, 378 const StatusCallback& callback) { 379 FileSystemURL filesystem_url(url_request->url()); 380 if (filesystem_url.type() == kFileSystemTypeExternal) { 381 for (size_t i = 0; i < auto_mount_handlers_.size(); i++) { 382 if (auto_mount_handlers_[i].Run(url_request, filesystem_url, 383 storage_domain, callback)) { 384 return; 385 } 386 } 387 } 388 callback.Run(base::File::FILE_ERROR_NOT_FOUND); 389 } 390 391 void FileSystemContext::DeleteFileSystem( 392 const GURL& origin_url, 393 FileSystemType type, 394 const StatusCallback& callback) { 395 DCHECK(io_task_runner_->RunsTasksOnCurrentThread()); 396 DCHECK(origin_url == origin_url.GetOrigin()); 397 DCHECK(!callback.is_null()); 398 399 FileSystemBackend* backend = GetFileSystemBackend(type); 400 if (!backend) { 401 callback.Run(base::File::FILE_ERROR_SECURITY); 402 return; 403 } 404 if (!backend->GetQuotaUtil()) { 405 callback.Run(base::File::FILE_ERROR_INVALID_OPERATION); 406 return; 407 } 408 409 base::PostTaskAndReplyWithResult( 410 default_file_task_runner(), 411 FROM_HERE, 412 // It is safe to pass Unretained(quota_util) since context owns it. 413 base::Bind(&FileSystemQuotaUtil::DeleteOriginDataOnFileTaskRunner, 414 base::Unretained(backend->GetQuotaUtil()), 415 make_scoped_refptr(this), 416 base::Unretained(quota_manager_proxy()), 417 origin_url, 418 type), 419 callback); 420 } 421 422 scoped_ptr<webkit_blob::FileStreamReader> 423 FileSystemContext::CreateFileStreamReader( 424 const FileSystemURL& url, 425 int64 offset, 426 const base::Time& expected_modification_time) { 427 if (!url.is_valid()) 428 return scoped_ptr<webkit_blob::FileStreamReader>(); 429 FileSystemBackend* backend = GetFileSystemBackend(url.type()); 430 if (!backend) 431 return scoped_ptr<webkit_blob::FileStreamReader>(); 432 return backend->CreateFileStreamReader( 433 url, offset, expected_modification_time, this); 434 } 435 436 scoped_ptr<FileStreamWriter> FileSystemContext::CreateFileStreamWriter( 437 const FileSystemURL& url, 438 int64 offset) { 439 if (!url.is_valid()) 440 return scoped_ptr<FileStreamWriter>(); 441 FileSystemBackend* backend = GetFileSystemBackend(url.type()); 442 if (!backend) 443 return scoped_ptr<FileStreamWriter>(); 444 return backend->CreateFileStreamWriter(url, offset, this); 445 } 446 447 scoped_ptr<FileSystemOperationRunner> 448 FileSystemContext::CreateFileSystemOperationRunner() { 449 return make_scoped_ptr(new FileSystemOperationRunner(this)); 450 } 451 452 FileSystemURL FileSystemContext::CrackURL(const GURL& url) const { 453 return CrackFileSystemURL(FileSystemURL(url)); 454 } 455 456 FileSystemURL FileSystemContext::CreateCrackedFileSystemURL( 457 const GURL& origin, 458 FileSystemType type, 459 const base::FilePath& path) const { 460 return CrackFileSystemURL(FileSystemURL(origin, type, path)); 461 } 462 463 #if defined(OS_CHROMEOS) 464 void FileSystemContext::EnableTemporaryFileSystemInIncognito() { 465 sandbox_backend_->set_enable_temporary_file_system_in_incognito(true); 466 } 467 #endif 468 469 bool FileSystemContext::CanServeURLRequest(const FileSystemURL& url) const { 470 // We never support accessing files in isolated filesystems via an URL. 471 if (url.mount_type() == kFileSystemTypeIsolated) 472 return false; 473 #if defined(OS_CHROMEOS) 474 if (url.type() == kFileSystemTypeTemporary && 475 sandbox_backend_->enable_temporary_file_system_in_incognito()) { 476 return true; 477 } 478 #endif 479 return !is_incognito_ || !FileSystemContext::IsSandboxFileSystem(url.type()); 480 } 481 482 bool FileSystemContext::ShouldFlushOnWriteCompletion( 483 FileSystemType type) const { 484 if (IsSandboxFileSystem(type)) { 485 // Disable Flush() for each write operation on SandboxFileSystems since it 486 // hurts the performance, assuming the FileSystems are stored in a local 487 // disk, we don't need to keep calling fsync() for it. 488 // On the other hand, other FileSystems that may stored on a removable media 489 // should be Flush()ed as soon as a write operation is completed, so that 490 // written data is saved over sudden media removal. 491 return false; 492 } 493 return true; 494 } 495 496 void FileSystemContext::OpenPluginPrivateFileSystem( 497 const GURL& origin_url, 498 FileSystemType type, 499 const std::string& filesystem_id, 500 const std::string& plugin_id, 501 OpenFileSystemMode mode, 502 const StatusCallback& callback) { 503 DCHECK(plugin_private_backend_); 504 plugin_private_backend_->OpenPrivateFileSystem( 505 origin_url, type, filesystem_id, plugin_id, mode, callback); 506 } 507 508 FileSystemContext::~FileSystemContext() { 509 } 510 511 void FileSystemContext::DeleteOnCorrectThread() const { 512 if (!io_task_runner_->RunsTasksOnCurrentThread() && 513 io_task_runner_->DeleteSoon(FROM_HERE, this)) { 514 return; 515 } 516 delete this; 517 } 518 519 FileSystemOperation* FileSystemContext::CreateFileSystemOperation( 520 const FileSystemURL& url, base::File::Error* error_code) { 521 if (!url.is_valid()) { 522 if (error_code) 523 *error_code = base::File::FILE_ERROR_INVALID_URL; 524 return NULL; 525 } 526 527 FileSystemBackend* backend = GetFileSystemBackend(url.type()); 528 if (!backend) { 529 if (error_code) 530 *error_code = base::File::FILE_ERROR_FAILED; 531 return NULL; 532 } 533 534 base::File::Error fs_error = base::File::FILE_OK; 535 FileSystemOperation* operation = 536 backend->CreateFileSystemOperation(url, this, &fs_error); 537 538 if (error_code) 539 *error_code = fs_error; 540 return operation; 541 } 542 543 FileSystemURL FileSystemContext::CrackFileSystemURL( 544 const FileSystemURL& url) const { 545 if (!url.is_valid()) 546 return FileSystemURL(); 547 548 // The returned value in case there is no crackers which can crack the url. 549 // This is valid situation for non isolated/external file systems. 550 FileSystemURL current = url; 551 552 // File system may be mounted multiple times (e.g., an isolated filesystem on 553 // top of an external filesystem). Hence cracking needs to be iterated. 554 for (;;) { 555 FileSystemURL cracked = current; 556 for (size_t i = 0; i < url_crackers_.size(); ++i) { 557 if (!url_crackers_[i]->HandlesFileSystemMountType(current.type())) 558 continue; 559 cracked = url_crackers_[i]->CrackFileSystemURL(current); 560 if (cracked.is_valid()) 561 break; 562 } 563 if (cracked == current) 564 break; 565 current = cracked; 566 } 567 return current; 568 } 569 570 void FileSystemContext::RegisterBackend(FileSystemBackend* backend) { 571 const FileSystemType mount_types[] = { 572 kFileSystemTypeTemporary, 573 kFileSystemTypePersistent, 574 kFileSystemTypeIsolated, 575 kFileSystemTypeExternal, 576 }; 577 // Register file system backends for public mount types. 578 for (size_t j = 0; j < ARRAYSIZE_UNSAFE(mount_types); ++j) { 579 if (backend->CanHandleType(mount_types[j])) { 580 const bool inserted = backend_map_.insert( 581 std::make_pair(mount_types[j], backend)).second; 582 DCHECK(inserted); 583 } 584 } 585 // Register file system backends for internal types. 586 for (int t = kFileSystemInternalTypeEnumStart + 1; 587 t < kFileSystemInternalTypeEnumEnd; ++t) { 588 FileSystemType type = static_cast<FileSystemType>(t); 589 if (backend->CanHandleType(type)) { 590 const bool inserted = backend_map_.insert( 591 std::make_pair(type, backend)).second; 592 DCHECK(inserted); 593 } 594 } 595 } 596 597 void FileSystemContext::DidOpenFileSystemForResolveURL( 598 const FileSystemURL& url, 599 const FileSystemContext::ResolveURLCallback& callback, 600 const GURL& filesystem_root, 601 const std::string& filesystem_name, 602 base::File::Error error) { 603 DCHECK(io_task_runner_->RunsTasksOnCurrentThread()); 604 605 if (error != base::File::FILE_OK) { 606 callback.Run(error, FileSystemInfo(), base::FilePath(), 607 FileSystemContext::RESOLVED_ENTRY_NOT_FOUND); 608 return; 609 } 610 611 fileapi::FileSystemInfo info( 612 filesystem_name, filesystem_root, url.mount_type()); 613 614 // Extract the virtual path not containing a filesystem type part from |url|. 615 base::FilePath parent = CrackURL(filesystem_root).virtual_path(); 616 base::FilePath child = url.virtual_path(); 617 base::FilePath path; 618 619 if (parent.empty()) { 620 path = child; 621 } else if (parent != child) { 622 bool result = parent.AppendRelativePath(child, &path); 623 DCHECK(result); 624 } 625 626 operation_runner()->GetMetadata( 627 url, base::Bind(&DidGetMetadataForResolveURL, path, callback, info)); 628 } 629 630 } // namespace fileapi 631