Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(96)

Side by Side Diff: chrome/browser/history/download_database.cc

Issue 13044019: Clean up entries left by crashes in the DownloadDB. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Shift cleanup to DownloadDatabase. Created 7 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 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 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "chrome/browser/history/download_database.h" 5 #include "chrome/browser/history/download_database.h"
6 6
7 #include <limits> 7 #include <limits>
8 #include <string> 8 #include <string>
9 #include <vector> 9 #include <vector>
10 10
(...skipping 12 matching lines...) Expand all
23 #include "content/public/browser/download_item.h" 23 #include "content/public/browser/download_item.h"
24 #include "sql/statement.h" 24 #include "sql/statement.h"
25 25
26 using content::DownloadItem; 26 using content::DownloadItem;
27 27
28 namespace history { 28 namespace history {
29 29
30 // static 30 // static
31 const int64 DownloadDatabase::kUninitializedHandle = -1; 31 const int64 DownloadDatabase::kUninitializedHandle = -1;
32 32
33 // These constants and the transformation functions below are used to allow
34 // DownloadItem::DownloadState and DownloadDangerType to change without
35 // breaking the database schema.
36 // They guarantee that the values of the |state| field in the database are one
37 // of the values returned by StateToInt, and that the values of the |state|
38 // field of the DownloadRows returned by QueryDownloads() are one of the values
39 // returned by IntToState().
40 const int DownloadDatabase::kStateInvalid = -1;
41 const int DownloadDatabase::kStateInProgress = 0;
42 const int DownloadDatabase::kStateComplete = 1;
43 const int DownloadDatabase::kStateCancelled = 2;
44 const int DownloadDatabase::kStateBug140687 = 3;
45 const int DownloadDatabase::kStateInterrupted = 4;
46
47 const int DownloadDatabase::kDangerTypeInvalid = -1;
48 const int DownloadDatabase::kDangerTypeNotDangerous = 0;
49 const int DownloadDatabase::kDangerTypeDangerousFile = 1;
50 const int DownloadDatabase::kDangerTypeDangerousUrl = 2;
51 const int DownloadDatabase::kDangerTypeDangerousContent = 3;
52 const int DownloadDatabase::kDangerTypeMaybeDangerousContent = 4;
53 const int DownloadDatabase::kDangerTypeUncommonContent = 5;
54 const int DownloadDatabase::kDangerTypeUserValidated = 6;
55 const int DownloadDatabase::kDangerTypeDangerousHost = 7;
56
33 namespace { 57 namespace {
34 58
35 // Reason for dropping a particular record. 59 // Reason for dropping a particular record.
36 enum DroppedReason { 60 enum DroppedReason {
37 DROPPED_REASON_BAD_STATE = 0, 61 DROPPED_REASON_BAD_STATE = 0,
38 DROPPED_REASON_BAD_DANGER_TYPE = 1, 62 DROPPED_REASON_BAD_DANGER_TYPE = 1,
39 DROPPED_REASON_MAX 63 DROPPED_REASON_MAX
40 }; 64 };
41 65
42 static const char kSchema[] = 66 static const char kSchema[] =
(...skipping 12 matching lines...) Expand all
55 79
56 static const char kUrlChainSchema[] = 80 static const char kUrlChainSchema[] =
57 "CREATE TABLE downloads_url_chains (" 81 "CREATE TABLE downloads_url_chains ("
58 "id INTEGER NOT NULL," // downloads.id. 82 "id INTEGER NOT NULL," // downloads.id.
59 "chain_index INTEGER NOT NULL," // Index of url in chain 83 "chain_index INTEGER NOT NULL," // Index of url in chain
60 // 0 is initial target, 84 // 0 is initial target,
61 // MAX is target after redirects. 85 // MAX is target after redirects.
62 "url LONGVARCHAR NOT NULL, " // URL. 86 "url LONGVARCHAR NOT NULL, " // URL.
63 "PRIMARY KEY (id, chain_index) )"; 87 "PRIMARY KEY (id, chain_index) )";
64 88
65 // These constants and next two functions are used to allow
66 // DownloadItem::DownloadState and DownloadDangerType to change without
67 // breaking the database schema.
68 // They guarantee that the values of the |state| field in the database are one
69 // of the values returned by StateToInt, and that the values of the |state|
70 // field of the DownloadRows returned by QueryDownloads() are one of the values
71 // returned by IntToState().
72 static const int kStateInvalid = -1;
73 static const int kStateInProgress = 0;
74 static const int kStateComplete = 1;
75 static const int kStateCancelled = 2;
76 static const int kStateBug140687 = 3;
77 static const int kStateInterrupted = 4;
78
79 static const int kDangerTypeInvalid = -1;
80 static const int kDangerTypeNotDangerous = 0;
81 static const int kDangerTypeDangerousFile = 1;
82 static const int kDangerTypeDangerousUrl = 2;
83 static const int kDangerTypeDangerousContent = 3;
84 static const int kDangerTypeMaybeDangerousContent = 4;
85 static const int kDangerTypeUncommonContent = 5;
86 static const int kDangerTypeUserValidated = 6;
87 static const int kDangerTypeDangerousHost = 7;
88
89 int StateToInt(DownloadItem::DownloadState state) { 89 int StateToInt(DownloadItem::DownloadState state) {
90 switch (state) { 90 switch (state) {
91 case DownloadItem::IN_PROGRESS: return kStateInProgress; 91 case DownloadItem::IN_PROGRESS: return DownloadDatabase::kStateInProgress;
92 case DownloadItem::COMPLETE: return kStateComplete; 92 case DownloadItem::COMPLETE: return DownloadDatabase::kStateComplete;
93 case DownloadItem::CANCELLED: return kStateCancelled; 93 case DownloadItem::CANCELLED: return DownloadDatabase::kStateCancelled;
94 case DownloadItem::INTERRUPTED: return kStateInterrupted; 94 case DownloadItem::INTERRUPTED: return DownloadDatabase::kStateInterrupted;
95 case DownloadItem::MAX_DOWNLOAD_STATE: 95 case DownloadItem::MAX_DOWNLOAD_STATE:
96 NOTREACHED(); 96 NOTREACHED();
97 return kStateInvalid; 97 return DownloadDatabase::kStateInvalid;
98 } 98 }
99 NOTREACHED(); 99 NOTREACHED();
100 return kStateInvalid; 100 return DownloadDatabase::kStateInvalid;
101 } 101 }
102 102
103 DownloadItem::DownloadState IntToState(int state) { 103 DownloadItem::DownloadState IntToState(int state) {
104 switch (state) { 104 switch (state) {
105 case kStateInProgress: return DownloadItem::IN_PROGRESS; 105 case DownloadDatabase::kStateInProgress: return DownloadItem::IN_PROGRESS;
106 case kStateComplete: return DownloadItem::COMPLETE; 106 case DownloadDatabase::kStateComplete: return DownloadItem::COMPLETE;
107 case kStateCancelled: return DownloadItem::CANCELLED; 107 case DownloadDatabase::kStateCancelled: return DownloadItem::CANCELLED;
108 // We should not need kStateBug140687 here because MigrateDownloadsState() 108 // We should not need kStateBug140687 here because MigrateDownloadsState()
109 // is called in HistoryDatabase::Init(). 109 // is called in HistoryDatabase::Init().
110 case kStateInterrupted: return DownloadItem::INTERRUPTED; 110 case DownloadDatabase::kStateInterrupted: return DownloadItem::INTERRUPTED;
111 default: return DownloadItem::MAX_DOWNLOAD_STATE; 111 default: return DownloadItem::MAX_DOWNLOAD_STATE;
112 } 112 }
113 } 113 }
114 114
115 int DangerTypeToInt(content::DownloadDangerType danger_type) { 115 int DangerTypeToInt(content::DownloadDangerType danger_type) {
116 switch (danger_type) { 116 switch (danger_type) {
117 case content::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS: 117 case content::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS:
118 return kDangerTypeNotDangerous; 118 return DownloadDatabase::kDangerTypeNotDangerous;
119 case content::DOWNLOAD_DANGER_TYPE_DANGEROUS_FILE: 119 case content::DOWNLOAD_DANGER_TYPE_DANGEROUS_FILE:
120 return kDangerTypeDangerousFile; 120 return DownloadDatabase::kDangerTypeDangerousFile;
121 case content::DOWNLOAD_DANGER_TYPE_DANGEROUS_URL: 121 case content::DOWNLOAD_DANGER_TYPE_DANGEROUS_URL:
122 return kDangerTypeDangerousUrl; 122 return DownloadDatabase::kDangerTypeDangerousUrl;
123 case content::DOWNLOAD_DANGER_TYPE_DANGEROUS_CONTENT: 123 case content::DOWNLOAD_DANGER_TYPE_DANGEROUS_CONTENT:
124 return kDangerTypeDangerousContent; 124 return DownloadDatabase::kDangerTypeDangerousContent;
125 case content::DOWNLOAD_DANGER_TYPE_MAYBE_DANGEROUS_CONTENT: 125 case content::DOWNLOAD_DANGER_TYPE_MAYBE_DANGEROUS_CONTENT:
126 return kDangerTypeMaybeDangerousContent; 126 return DownloadDatabase::kDangerTypeMaybeDangerousContent;
127 case content::DOWNLOAD_DANGER_TYPE_UNCOMMON_CONTENT: 127 case content::DOWNLOAD_DANGER_TYPE_UNCOMMON_CONTENT:
128 return kDangerTypeUncommonContent; 128 return DownloadDatabase::kDangerTypeUncommonContent;
129 case content::DOWNLOAD_DANGER_TYPE_USER_VALIDATED: 129 case content::DOWNLOAD_DANGER_TYPE_USER_VALIDATED:
130 return kDangerTypeUserValidated; 130 return DownloadDatabase::kDangerTypeUserValidated;
131 case content::DOWNLOAD_DANGER_TYPE_DANGEROUS_HOST: 131 case content::DOWNLOAD_DANGER_TYPE_DANGEROUS_HOST:
132 return kDangerTypeDangerousHost; 132 return DownloadDatabase::kDangerTypeDangerousHost;
133 case content::DOWNLOAD_DANGER_TYPE_MAX: 133 case content::DOWNLOAD_DANGER_TYPE_MAX:
134 NOTREACHED(); 134 NOTREACHED();
135 return kDangerTypeInvalid; 135 return DownloadDatabase::kDangerTypeInvalid;
136 } 136 }
137 NOTREACHED(); 137 NOTREACHED();
138 return kDangerTypeInvalid; 138 return DownloadDatabase::kDangerTypeInvalid;
139 } 139 }
140 140
141 content::DownloadDangerType IntToDangerType(int danger_type) { 141 content::DownloadDangerType IntToDangerType(int danger_type) {
142 switch (danger_type) { 142 switch (danger_type) {
143 case kDangerTypeNotDangerous: 143 case DownloadDatabase::kDangerTypeNotDangerous:
144 return content::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS; 144 return content::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS;
145 case kDangerTypeDangerousFile: 145 case DownloadDatabase::kDangerTypeDangerousFile:
146 return content::DOWNLOAD_DANGER_TYPE_DANGEROUS_FILE; 146 return content::DOWNLOAD_DANGER_TYPE_DANGEROUS_FILE;
147 case kDangerTypeDangerousUrl: 147 case DownloadDatabase::kDangerTypeDangerousUrl:
148 return content::DOWNLOAD_DANGER_TYPE_DANGEROUS_URL; 148 return content::DOWNLOAD_DANGER_TYPE_DANGEROUS_URL;
149 case kDangerTypeDangerousContent: 149 case DownloadDatabase::kDangerTypeDangerousContent:
150 return content::DOWNLOAD_DANGER_TYPE_DANGEROUS_CONTENT; 150 return content::DOWNLOAD_DANGER_TYPE_DANGEROUS_CONTENT;
151 case kDangerTypeMaybeDangerousContent: 151 case DownloadDatabase::kDangerTypeMaybeDangerousContent:
152 return content::DOWNLOAD_DANGER_TYPE_MAYBE_DANGEROUS_CONTENT; 152 return content::DOWNLOAD_DANGER_TYPE_MAYBE_DANGEROUS_CONTENT;
153 case kDangerTypeUncommonContent: 153 case DownloadDatabase::kDangerTypeUncommonContent:
154 return content::DOWNLOAD_DANGER_TYPE_UNCOMMON_CONTENT; 154 return content::DOWNLOAD_DANGER_TYPE_UNCOMMON_CONTENT;
155 case kDangerTypeUserValidated: 155 case DownloadDatabase::kDangerTypeUserValidated:
156 return content::DOWNLOAD_DANGER_TYPE_USER_VALIDATED; 156 return content::DOWNLOAD_DANGER_TYPE_USER_VALIDATED;
157 case kDangerTypeDangerousHost: 157 case DownloadDatabase::kDangerTypeDangerousHost:
158 return content::DOWNLOAD_DANGER_TYPE_DANGEROUS_HOST; 158 return content::DOWNLOAD_DANGER_TYPE_DANGEROUS_HOST;
159 default: 159 default:
160 return content::DOWNLOAD_DANGER_TYPE_MAX; 160 return content::DOWNLOAD_DANGER_TYPE_MAX;
161 } 161 }
162 } 162 }
163 163
164 #if defined(OS_POSIX) 164 #if defined(OS_POSIX)
165 165
166 // Binds/reads the given file path to the given column of the given statement. 166 // Binds/reads the given file path to the given column of the given statement.
167 void BindFilePath(sql::Statement& statement, const base::FilePath& path, 167 void BindFilePath(sql::Statement& statement, const base::FilePath& path,
(...skipping 20 matching lines...) Expand all
188 // Key in the meta_table containing the next id to use for a new download in 188 // Key in the meta_table containing the next id to use for a new download in
189 // this profile. 189 // this profile.
190 static const char kNextDownloadId[] = "next_download_id"; 190 static const char kNextDownloadId[] = "next_download_id";
191 191
192 } // namespace 192 } // namespace
193 193
194 DownloadDatabase::DownloadDatabase() 194 DownloadDatabase::DownloadDatabase()
195 : owning_thread_set_(false), 195 : owning_thread_set_(false),
196 owning_thread_(0), 196 owning_thread_(0),
197 next_id_(0), 197 next_id_(0),
198 next_db_handle_(0) { 198 next_db_handle_(0),
199 in_progress_entry_cleanup_completed_(false) {
199 } 200 }
200 201
201 DownloadDatabase::~DownloadDatabase() { 202 DownloadDatabase::~DownloadDatabase() {
202 } 203 }
203 204
204 bool DownloadDatabase::EnsureColumnExists( 205 bool DownloadDatabase::EnsureColumnExists(
205 const std::string& name, const std::string& type) { 206 const std::string& name, const std::string& type) {
206 std::string add_col = "ALTER TABLE downloads ADD COLUMN " + name + " " + type; 207 std::string add_col = "ALTER TABLE downloads ADD COLUMN " + name + " " + type;
207 return GetDB().DoesColumnExist("downloads", name.c_str()) || 208 return GetDB().DoesColumnExist("downloads", name.c_str()) ||
208 GetDB().Execute(add_col.c_str()); 209 GetDB().Execute(add_col.c_str());
(...skipping 67 matching lines...) Expand 10 before | Expand all | Expand 10 after
276 GetDB().Execute(kSchema) && GetDB().Execute(kUrlChainSchema)); 277 GetDB().Execute(kSchema) && GetDB().Execute(kUrlChainSchema));
277 } 278 }
278 } 279 }
279 280
280 bool DownloadDatabase::DropDownloadTable() { 281 bool DownloadDatabase::DropDownloadTable() {
281 return GetDB().Execute("DROP TABLE downloads"); 282 return GetDB().Execute("DROP TABLE downloads");
282 } 283 }
283 284
284 void DownloadDatabase::QueryDownloads( 285 void DownloadDatabase::QueryDownloads(
285 std::vector<DownloadRow>* results) { 286 std::vector<DownloadRow>* results) {
287 if (!in_progress_entry_cleanup_completed_)
288 CleanUpInProgressEntries();
289
286 results->clear(); 290 results->clear();
287 if (next_db_handle_ < 1) 291 if (next_db_handle_ < 1)
288 next_db_handle_ = 1; 292 next_db_handle_ = 1;
289 std::set<int64> db_handles; 293 std::set<int64> db_handles;
290 294
291 std::map<DownloadID, DownloadRow*> info_map; 295 std::map<DownloadID, DownloadRow*> info_map;
292 296
293 sql::Statement statement_main(GetDB().GetCachedStatement(SQL_FROM_HERE, 297 sql::Statement statement_main(GetDB().GetCachedStatement(SQL_FROM_HERE,
294 "SELECT id, current_path, target_path, start_time, received_bytes, " 298 "SELECT id, current_path, target_path, start_time, received_bytes, "
295 "total_bytes, state, danger_type, interrupt_reason, end_time, opened " 299 "total_bytes, state, danger_type, interrupt_reason, end_time, opened "
(...skipping 91 matching lines...) Expand 10 before | Expand all | Expand 10 after
387 for (std::map<DownloadID, DownloadRow*>::iterator 391 for (std::map<DownloadID, DownloadRow*>::iterator
388 it = info_map.begin(); it != info_map.end(); ++it) { 392 it = info_map.begin(); it != info_map.end(); ++it) {
389 // Copy the contents of the stored info. 393 // Copy the contents of the stored info.
390 results->push_back(*it->second); 394 results->push_back(*it->second);
391 delete it->second; 395 delete it->second;
392 it->second = NULL; 396 it->second = NULL;
393 } 397 }
394 } 398 }
395 399
396 bool DownloadDatabase::UpdateDownload(const DownloadRow& data) { 400 bool DownloadDatabase::UpdateDownload(const DownloadRow& data) {
401 if (!in_progress_entry_cleanup_completed_)
402 CleanUpInProgressEntries();
403
397 DCHECK(data.db_handle > 0); 404 DCHECK(data.db_handle > 0);
398 int state = StateToInt(data.state); 405 int state = StateToInt(data.state);
399 if (state == kStateInvalid) { 406 if (state == kStateInvalid) {
400 NOTREACHED(); 407 NOTREACHED();
401 return false; 408 return false;
402 } 409 }
403 int danger_type = DangerTypeToInt(data.danger_type); 410 int danger_type = DangerTypeToInt(data.danger_type);
404 if (danger_type == kDangerTypeInvalid) { 411 if (danger_type == kDangerTypeInvalid) {
405 NOTREACHED(); 412 NOTREACHED();
406 return false; 413 return false;
(...skipping 12 matching lines...) Expand all
419 statement.BindInt(column++, danger_type); 426 statement.BindInt(column++, danger_type);
420 statement.BindInt(column++, static_cast<int>(data.interrupt_reason)); 427 statement.BindInt(column++, static_cast<int>(data.interrupt_reason));
421 statement.BindInt64(column++, data.end_time.ToInternalValue()); 428 statement.BindInt64(column++, data.end_time.ToInternalValue());
422 statement.BindInt(column++, data.total_bytes); 429 statement.BindInt(column++, data.total_bytes);
423 statement.BindInt(column++, (data.opened ? 1 : 0)); 430 statement.BindInt(column++, (data.opened ? 1 : 0));
424 statement.BindInt64(column++, data.db_handle); 431 statement.BindInt64(column++, data.db_handle);
425 432
426 return statement.Run(); 433 return statement.Run();
427 } 434 }
428 435
429 bool DownloadDatabase::CleanUpInProgressEntries() { 436 void DownloadDatabase::CleanUpInProgressEntries() {
430 sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE, 437 sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
431 "UPDATE downloads SET state=? WHERE state=?")); 438 "UPDATE downloads SET state=?, interrupt_reason=? WHERE state=?"));
432 statement.BindInt(0, kStateCancelled); 439 statement.BindInt(0, kStateInterrupted);
433 statement.BindInt(1, kStateInProgress); 440 statement.BindInt(1, content::DOWNLOAD_INTERRUPT_REASON_CRASH);
441 statement.BindInt(2, kStateInProgress);
434 442
435 return statement.Run(); 443 statement.Run();
444 in_progress_entry_cleanup_completed_ = true;
436 } 445 }
437 446
438 int64 DownloadDatabase::CreateDownload( 447 int64 DownloadDatabase::CreateDownload(
439 const DownloadRow& info) { 448 const DownloadRow& info) {
449 if (!in_progress_entry_cleanup_completed_)
450 CleanUpInProgressEntries();
451
440 if (next_db_handle_ == 0) { 452 if (next_db_handle_ == 0) {
441 // This is unlikely. All current known tests and users already call 453 // This is unlikely. All current known tests and users already call
442 // QueryDownloads() before CreateDownload(). 454 // QueryDownloads() before CreateDownload().
443 std::vector<DownloadRow> results; 455 std::vector<DownloadRow> results;
444 QueryDownloads(&results); 456 QueryDownloads(&results);
445 CHECK_NE(0, next_db_handle_); 457 CHECK_NE(0, next_db_handle_);
446 } 458 }
447 459
448 int state = StateToInt(info.state); 460 int state = StateToInt(info.state);
449 if (state == kStateInvalid) 461 if (state == kStateInvalid)
(...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after
498 statement_insert_chain.Reset(true); 510 statement_insert_chain.Reset(true);
499 } 511 }
500 512
501 // TODO(benjhayden) if(info.id>next_id_){setvalue;next_id_=info.id;} 513 // TODO(benjhayden) if(info.id>next_id_){setvalue;next_id_=info.id;}
502 GetMetaTable().SetValue(kNextDownloadId, ++next_id_); 514 GetMetaTable().SetValue(kNextDownloadId, ++next_id_);
503 515
504 return db_handle; 516 return db_handle;
505 } 517 }
506 518
507 void DownloadDatabase::RemoveDownload(int64 handle) { 519 void DownloadDatabase::RemoveDownload(int64 handle) {
520 if (!in_progress_entry_cleanup_completed_)
521 CleanUpInProgressEntries();
522
508 sql::Statement downloads_statement(GetDB().GetCachedStatement(SQL_FROM_HERE, 523 sql::Statement downloads_statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
509 "DELETE FROM downloads WHERE id=?")); 524 "DELETE FROM downloads WHERE id=?"));
510 downloads_statement.BindInt64(0, handle); 525 downloads_statement.BindInt64(0, handle);
511 downloads_statement.Run(); 526 downloads_statement.Run();
512 527
513 sql::Statement urlchain_statement(GetDB().GetCachedStatement(SQL_FROM_HERE, 528 sql::Statement urlchain_statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
514 "DELETE FROM downloads_url_chains WHERE id=?")); 529 "DELETE FROM downloads_url_chains WHERE id=?"));
515 urlchain_statement.BindInt64(0, handle); 530 urlchain_statement.BindInt64(0, handle);
516 urlchain_statement.Run(); 531 urlchain_statement.Run();
517 } 532 }
518 533
519 int DownloadDatabase::CountDownloads() { 534 int DownloadDatabase::CountDownloads() {
535 if (!in_progress_entry_cleanup_completed_)
536 CleanUpInProgressEntries();
537
520 sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE, 538 sql::Statement statement(GetDB().GetCachedStatement(SQL_FROM_HERE,
521 "SELECT count(*) from downloads")); 539 "SELECT count(*) from downloads"));
522 statement.Step(); 540 statement.Step();
523 return statement.ColumnInt(0); 541 return statement.ColumnInt(0);
524 } 542 }
525 543
526 } // namespace history 544 } // namespace history
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698