Improve how attachments are saved to the db and disk.
This mostly aims to make the Geary.Attachment and ImapDB.Attachment objects usable more generally for managing attachments, allowing these to be instantiated once, persisted, and then reused, rather than going through a number of representations (GMime, SQlite, Geary) and having to be saved and re-loaded. * src/engine/api/geary-attachment.vala (Attachment): Remove id property and allow both file and filesize properties to be set after instances are constructed. Update call sites. * src/engine/api/geary-email.vala (Email): Remove get_attachment_by_id since it unused. * src/engine/imap-db/imap-db-attachment.vala (Attachment): Chase Geary.Attachment API changes, move object-specific persistence code into methods on the actual object class itself and modernise a bit. Rename static methods to be a bit more terse. Update call sites and add unit tests. * src/engine/imap-db/imap-db-folder.vala (Folder): Rather than saving attachments to the db then reloading them to add them to their email objects, just instantiate Attachment instances once, save and then add them. * src/engine/imap-db/imap-db-gc.vala (GC): Replace custom SQL with existing accessor for listing attachments. * src/engine/util/util-stream.vala (MimeOutputStream): New adaptor class for GMime streams to GIO output streams.
This commit is contained in:
parent
15d8789685
commit
037af00740
12 changed files with 619 additions and 354 deletions
|
|
@ -10,7 +10,6 @@ extern const string _SOURCE_ROOT_DIR;
|
|||
|
||||
class Geary.AttachmentTest : TestCase {
|
||||
|
||||
private const string ATTACHMENT_ID = "test-id";
|
||||
private const string CONTENT_TYPE = "image/png";
|
||||
private const string CONTENT_ID = "test-content-id";
|
||||
private const string CONTENT_DESC = "Mea navis volitans anguillis plena est";
|
||||
|
|
@ -21,19 +20,19 @@ class Geary.AttachmentTest : TestCase {
|
|||
private Mime.ContentDisposition? content_disposition;
|
||||
private File? file;
|
||||
|
||||
|
||||
private class TestAttachment : Attachment {
|
||||
// A test article
|
||||
|
||||
internal TestAttachment(string id,
|
||||
Mime.ContentType content_type,
|
||||
internal TestAttachment(Mime.ContentType content_type,
|
||||
string? content_id,
|
||||
string? content_description,
|
||||
Mime.ContentDisposition content_disposition,
|
||||
string? content_filename,
|
||||
File file,
|
||||
int64 filesize) {
|
||||
base(id, content_type, content_id, content_description,
|
||||
content_disposition, content_filename, file, filesize);
|
||||
GLib.File file) {
|
||||
base(content_type, content_id, content_description,
|
||||
content_disposition, content_filename);
|
||||
set_file_info(file, 742);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -76,14 +75,12 @@ class Geary.AttachmentTest : TestCase {
|
|||
public void get_safe_file_name_with_content_name() throws Error {
|
||||
const string TEST_FILENAME = "test-filename.png";
|
||||
Attachment test = new TestAttachment(
|
||||
ATTACHMENT_ID,
|
||||
this.content_type,
|
||||
CONTENT_ID,
|
||||
CONTENT_DESC,
|
||||
content_disposition,
|
||||
TEST_FILENAME,
|
||||
this.file,
|
||||
742
|
||||
this.file
|
||||
);
|
||||
|
||||
test.get_safe_file_name.begin(null, (obj, ret) => {
|
||||
|
|
@ -97,14 +94,12 @@ class Geary.AttachmentTest : TestCase {
|
|||
const string TEST_FILENAME = "test-filename.jpg";
|
||||
const string RESULT_FILENAME = "test-filename.jpg.png";
|
||||
Attachment test = new TestAttachment(
|
||||
ATTACHMENT_ID,
|
||||
this.content_type,
|
||||
CONTENT_ID,
|
||||
CONTENT_DESC,
|
||||
content_disposition,
|
||||
TEST_FILENAME,
|
||||
this.file,
|
||||
742
|
||||
this.file
|
||||
);
|
||||
|
||||
test.get_safe_file_name.begin(null, (obj, ret) => {
|
||||
|
|
@ -118,14 +113,12 @@ class Geary.AttachmentTest : TestCase {
|
|||
const string TEST_FILENAME = "test-filename";
|
||||
const string RESULT_FILENAME = "test-filename.png";
|
||||
Attachment test = new TestAttachment(
|
||||
ATTACHMENT_ID,
|
||||
this.content_type,
|
||||
CONTENT_ID,
|
||||
CONTENT_DESC,
|
||||
content_disposition,
|
||||
TEST_FILENAME,
|
||||
this.file,
|
||||
742
|
||||
this.file
|
||||
);
|
||||
|
||||
test.get_safe_file_name.begin(null, (obj, ret) => {
|
||||
|
|
@ -138,14 +131,12 @@ class Geary.AttachmentTest : TestCase {
|
|||
public void get_safe_file_name_with_no_content_name() throws Error {
|
||||
const string RESULT_FILENAME = CONTENT_ID + ".png";
|
||||
Attachment test = new TestAttachment(
|
||||
ATTACHMENT_ID,
|
||||
this.content_type,
|
||||
CONTENT_ID,
|
||||
CONTENT_DESC,
|
||||
content_disposition,
|
||||
null,
|
||||
this.file,
|
||||
742
|
||||
this.file
|
||||
);
|
||||
|
||||
test.get_safe_file_name.begin(null, (obj, ret) => {
|
||||
|
|
@ -156,16 +147,14 @@ class Geary.AttachmentTest : TestCase {
|
|||
}
|
||||
|
||||
public void get_safe_file_name_with_no_content_name_or_id() throws Error {
|
||||
const string RESULT_FILENAME = ATTACHMENT_ID + ".png";
|
||||
const string RESULT_FILENAME = "attachment.png";
|
||||
Attachment test = new TestAttachment(
|
||||
ATTACHMENT_ID,
|
||||
this.content_type,
|
||||
null,
|
||||
CONTENT_DESC,
|
||||
content_disposition,
|
||||
null,
|
||||
this.file,
|
||||
742
|
||||
this.file
|
||||
);
|
||||
|
||||
test.get_safe_file_name.begin(null, (obj, ret) => {
|
||||
|
|
@ -179,14 +168,12 @@ class Geary.AttachmentTest : TestCase {
|
|||
const string ALT_TEXT = "some text";
|
||||
const string RESULT_FILENAME = "some text.png";
|
||||
Attachment test = new TestAttachment(
|
||||
ATTACHMENT_ID,
|
||||
this.content_type,
|
||||
null,
|
||||
CONTENT_DESC,
|
||||
content_disposition,
|
||||
null,
|
||||
this.file,
|
||||
742
|
||||
this.file
|
||||
);
|
||||
|
||||
test.get_safe_file_name.begin(ALT_TEXT, (obj, ret) => {
|
||||
|
|
@ -199,14 +186,12 @@ class Geary.AttachmentTest : TestCase {
|
|||
public void get_safe_file_name_with_default_content_type() throws Error {
|
||||
const string TEST_FILENAME = "test-filename.png";
|
||||
Attachment test = new TestAttachment(
|
||||
ATTACHMENT_ID,
|
||||
this.default_type,
|
||||
CONTENT_ID,
|
||||
CONTENT_DESC,
|
||||
content_disposition,
|
||||
TEST_FILENAME,
|
||||
this.file,
|
||||
742
|
||||
this.file
|
||||
);
|
||||
|
||||
test.get_safe_file_name.begin(null, (obj, ret) => {
|
||||
|
|
@ -221,14 +206,12 @@ class Geary.AttachmentTest : TestCase {
|
|||
const string TEST_FILENAME = "test-filename.jpg";
|
||||
const string RESULT_FILENAME = "test-filename.jpg.png";
|
||||
Attachment test = new TestAttachment(
|
||||
ATTACHMENT_ID,
|
||||
this.default_type,
|
||||
CONTENT_ID,
|
||||
CONTENT_DESC,
|
||||
content_disposition,
|
||||
TEST_FILENAME,
|
||||
this.file,
|
||||
742
|
||||
this.file
|
||||
);
|
||||
|
||||
test.get_safe_file_name.begin(null, (obj, ret) => {
|
||||
|
|
@ -242,14 +225,12 @@ class Geary.AttachmentTest : TestCase {
|
|||
throws Error {
|
||||
const string TEST_FILENAME = "test-filename.unlikely";
|
||||
Attachment test = new TestAttachment(
|
||||
ATTACHMENT_ID,
|
||||
this.default_type,
|
||||
CONTENT_ID,
|
||||
CONTENT_DESC,
|
||||
content_disposition,
|
||||
TEST_FILENAME,
|
||||
File.new_for_path(TEST_FILENAME),
|
||||
742
|
||||
File.new_for_path(TEST_FILENAME)
|
||||
);
|
||||
|
||||
test.get_safe_file_name.begin(null, (obj, ret) => {
|
||||
|
|
|
|||
|
|
@ -5,17 +5,102 @@
|
|||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
private const string TEXT_ATTACHMENT = "This is an attachment.\n";
|
||||
|
||||
|
||||
class Geary.ImapDB.AttachmentTest : TestCase {
|
||||
|
||||
|
||||
public AttachmentTest() {
|
||||
base("Geary.ImapDB.AttachmentTest");
|
||||
add_test("new_from_minimal_mime_part", new_from_minimal_mime_part);
|
||||
add_test("new_from_complete_mime_part", new_from_complete_mime_part);
|
||||
add_test("new_from_inline_mime_part", new_from_inline_mime_part);
|
||||
}
|
||||
|
||||
public void new_from_minimal_mime_part() throws Error {
|
||||
GMime.Part part = new_part(null, TEXT_ATTACHMENT.data);
|
||||
part.set_header("Content-Type", "");
|
||||
|
||||
Attachment test = new Attachment.from_part(1, part);
|
||||
assert_string(
|
||||
Geary.Mime.ContentType.DEFAULT_CONTENT_TYPE,
|
||||
test.content_type.to_string()
|
||||
);
|
||||
assert_null_string(test.content_id, "content_id");
|
||||
assert_null_string(test.content_description, "content_description");
|
||||
assert_int(
|
||||
Geary.Mime.DispositionType.UNSPECIFIED,
|
||||
test.content_disposition.disposition_type,
|
||||
"content disposition type"
|
||||
);
|
||||
assert_false(test.has_content_filename, "has_content_filename");
|
||||
assert_null_string(test.content_filename, "content_filename");
|
||||
}
|
||||
|
||||
public void new_from_complete_mime_part() throws Error {
|
||||
const string TYPE = "text/plain";
|
||||
const string ID = "test-id";
|
||||
const string DESC = "test description";
|
||||
const string NAME = "test.txt";
|
||||
|
||||
GMime.Part part = new_part(null, TEXT_ATTACHMENT.data);
|
||||
part.set_content_id(ID);
|
||||
part.set_content_description(DESC);
|
||||
part.set_content_disposition(
|
||||
new GMime.ContentDisposition.from_string(
|
||||
"attachment; filename=%s".printf(NAME)
|
||||
)
|
||||
);
|
||||
|
||||
Attachment test = new Attachment.from_part(1, part);
|
||||
|
||||
assert_string(TYPE, test.content_type.to_string());
|
||||
assert_string(ID, test.content_id);
|
||||
assert_string(DESC, test.content_description);
|
||||
assert_int(
|
||||
Geary.Mime.DispositionType.ATTACHMENT,
|
||||
test.content_disposition.disposition_type
|
||||
);
|
||||
assert_true(test.has_content_filename, "has_content_filename");
|
||||
assert_string(test.content_filename, NAME, "content_filename");
|
||||
}
|
||||
|
||||
public void new_from_inline_mime_part() throws Error {
|
||||
GMime.Part part = new_part(null, TEXT_ATTACHMENT.data);
|
||||
part.set_content_disposition(
|
||||
new GMime.ContentDisposition.from_string("inline")
|
||||
);
|
||||
|
||||
Attachment test = new Attachment.from_part(1, part);
|
||||
|
||||
assert_int(
|
||||
Geary.Mime.DispositionType.INLINE,
|
||||
test.content_disposition.disposition_type
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Geary.ImapDB.AttachmentIoTest : TestCase {
|
||||
|
||||
|
||||
private GLib.File? tmp_dir;
|
||||
private Geary.Db.Database? db;
|
||||
|
||||
public AttachmentTest() {
|
||||
base("Geary.ImapDB.FolderTest");
|
||||
public AttachmentIoTest() {
|
||||
base("Geary.ImapDB.AttachmentIoTest");
|
||||
add_test("save_minimal_attachment", save_minimal_attachment);
|
||||
add_test("save_complete_attachment", save_complete_attachment);
|
||||
add_test("list_attachments", list_attachments);
|
||||
add_test("delete_attachments", delete_attachments);
|
||||
}
|
||||
|
||||
public override void set_up() throws Error {
|
||||
this.tmp_dir = GLib.File.new_for_path(
|
||||
GLib.DirUtils.make_tmp("geary-impadb-attachment-io-test-XXXXXX")
|
||||
);
|
||||
|
||||
this.db = new Geary.Db.Database.transient();
|
||||
this.db.open.begin(
|
||||
Geary.Db.DatabaseFlags.NONE, null, null,
|
||||
|
|
@ -47,43 +132,184 @@ CREATE TABLE MessageAttachmentTable (
|
|||
public override void tear_down() throws Error {
|
||||
this.db.close();
|
||||
this.db = null;
|
||||
|
||||
Geary.Files.recursive_delete_async.begin(
|
||||
this.tmp_dir,
|
||||
null,
|
||||
(obj, res) => { async_complete(res); }
|
||||
);
|
||||
Geary.Files.recursive_delete_async.end(async_result());
|
||||
this.tmp_dir = null;
|
||||
}
|
||||
|
||||
public void save_minimal_attachment() throws Error {
|
||||
GLib.File tmp_dir = GLib.File.new_for_path(
|
||||
GLib.DirUtils.make_tmp("geary-impadb-foldertest-XXXXXX")
|
||||
);
|
||||
GMime.Part part = new_part(null, TEXT_ATTACHMENT.data);
|
||||
|
||||
GMime.DataWrapper body = new GMime.DataWrapper.with_stream(
|
||||
new GMime.StreamMem.with_buffer(TEXT_ATTACHMENT.data),
|
||||
GMime.ContentEncoding.DEFAULT
|
||||
);
|
||||
GMime.Part attachment = new GMime.Part.with_type("text", "plain");
|
||||
attachment.set_content_object(body);
|
||||
attachment.encode(GMime.EncodingConstraint.7BIT);
|
||||
|
||||
Gee.List<GMime.Part> attachments = new Gee.LinkedList<GMime.Part>();
|
||||
attachments.add(attachment);
|
||||
|
||||
Geary.ImapDB.Attachment.do_save_attachments(
|
||||
Gee.List<Attachment> attachments = Attachment.save_attachments(
|
||||
this.db.get_master_connection(),
|
||||
tmp_dir,
|
||||
this.tmp_dir,
|
||||
1,
|
||||
attachments,
|
||||
new Gee.ArrayList<GMime.Part>.wrap({ part }),
|
||||
null
|
||||
);
|
||||
|
||||
assert_int(1, attachments.size, "No attachment provided");
|
||||
|
||||
Geary.Attachment attachment = attachments[0];
|
||||
assert_non_null(attachment.file, "Attachment file");
|
||||
assert_int(
|
||||
TEXT_ATTACHMENT.data.length,
|
||||
(int) attachment.filesize,
|
||||
"Attachment file size"
|
||||
);
|
||||
|
||||
uint8[] buf = new uint8[4096];
|
||||
size_t len = 0;
|
||||
attachments[0].file.read().read_all(buf, out len);
|
||||
assert_string(TEXT_ATTACHMENT, (string) buf[0:len]);
|
||||
|
||||
Geary.Db.Result result = this.db.query(
|
||||
"SELECT * FROM MessageAttachmentTable;"
|
||||
);
|
||||
assert_false(result.finished, "Row not inserted");
|
||||
assert_int(1, result.int_for("message_id"), "Message id");
|
||||
assert_int(1, result.int_for("message_id"), "Row message id");
|
||||
assert_int(
|
||||
TEXT_ATTACHMENT.data.length,
|
||||
result.int_for("filesize"),
|
||||
"Row file size"
|
||||
);
|
||||
assert_false(result.next(), "Multiple rows inserted");
|
||||
|
||||
Geary.Files.recursive_delete_async.begin(tmp_dir);
|
||||
}
|
||||
|
||||
public void save_complete_attachment() throws Error {
|
||||
const string TYPE = "text/plain";
|
||||
const string ID = "test-id";
|
||||
const string DESCRIPTION = "test description";
|
||||
const Geary.Mime.DispositionType DISPOSITION_TYPE =
|
||||
Geary.Mime.DispositionType.INLINE;
|
||||
const string FILENAME = "test.txt";
|
||||
|
||||
private const string TEXT_ATTACHMENT = "This is an attachment.\n";
|
||||
GMime.Part part = new_part(TYPE, TEXT_ATTACHMENT.data);
|
||||
part.set_content_id(ID);
|
||||
part.set_content_description(DESCRIPTION);
|
||||
part.set_content_disposition(
|
||||
new GMime.ContentDisposition.from_string(
|
||||
"inline; filename=%s;".printf(FILENAME)
|
||||
));
|
||||
|
||||
Gee.List<Attachment> attachments = Attachment.save_attachments(
|
||||
this.db.get_master_connection(),
|
||||
this.tmp_dir,
|
||||
1,
|
||||
new Gee.ArrayList<GMime.Part>.wrap({ part }),
|
||||
null
|
||||
);
|
||||
|
||||
assert_int(1, attachments.size, "No attachment provided");
|
||||
|
||||
Geary.Attachment attachment = attachments[0];
|
||||
assert_string(TYPE, attachment.content_type.to_string());
|
||||
assert_string(ID, attachment.content_id);
|
||||
assert_string(DESCRIPTION, attachment.content_description);
|
||||
assert_string(FILENAME, attachment.content_filename);
|
||||
assert_int(
|
||||
DISPOSITION_TYPE,
|
||||
attachment.content_disposition.disposition_type,
|
||||
"Attachment disposition type"
|
||||
);
|
||||
|
||||
uint8[] buf = new uint8[4096];
|
||||
size_t len = 0;
|
||||
attachment.file.read().read_all(buf, out len);
|
||||
assert_string(TEXT_ATTACHMENT, (string) buf[0:len]);
|
||||
|
||||
Geary.Db.Result result = this.db.query(
|
||||
"SELECT * FROM MessageAttachmentTable;"
|
||||
);
|
||||
assert_false(result.finished, "Row not inserted");
|
||||
assert_int(1, result.int_for("message_id"), "Row message id");
|
||||
assert_string(TYPE, result.string_for("mime_type"));
|
||||
assert_string(ID, result.string_for("content_id"));
|
||||
assert_string(DESCRIPTION, result.string_for("description"));
|
||||
assert_int(
|
||||
DISPOSITION_TYPE,
|
||||
result.int_for("disposition"),
|
||||
"Row disposition type"
|
||||
);
|
||||
assert_string(FILENAME, result.string_for("filename"));
|
||||
assert_false(result.next(), "Multiple rows inserted");
|
||||
|
||||
}
|
||||
|
||||
public void list_attachments() throws Error {
|
||||
this.db.exec("""
|
||||
INSERT INTO MessageAttachmentTable ( message_id, mime_type )
|
||||
VALUES (1, 'text/plain');
|
||||
""");
|
||||
this.db.exec("""
|
||||
INSERT INTO MessageAttachmentTable ( message_id, mime_type )
|
||||
VALUES (2, 'text/plain');
|
||||
""");
|
||||
|
||||
Gee.List<Attachment> loaded = Attachment.list_attachments(
|
||||
this.db.get_master_connection(),
|
||||
GLib.File.new_for_path("/tmp"),
|
||||
1,
|
||||
null
|
||||
);
|
||||
|
||||
assert_int(1, loaded.size, "Expected one row loaded");
|
||||
assert_int(1, (int) loaded[0].message_id, "Unexpected message id");
|
||||
}
|
||||
|
||||
public void delete_attachments() throws Error {
|
||||
GMime.Part part = new_part(null, TEXT_ATTACHMENT.data);
|
||||
|
||||
Gee.List<Attachment> attachments = Attachment.save_attachments(
|
||||
this.db.get_master_connection(),
|
||||
this.tmp_dir,
|
||||
1,
|
||||
new Gee.ArrayList<GMime.Part>.wrap({ part }),
|
||||
null
|
||||
);
|
||||
|
||||
assert_true(attachments[0].file.query_exists(null),
|
||||
"Attachment not saved to disk");
|
||||
|
||||
this.db.exec("""
|
||||
INSERT INTO MessageAttachmentTable ( message_id, mime_type )
|
||||
VALUES (2, 'text/plain');
|
||||
""");
|
||||
|
||||
Attachment.delete_attachments(
|
||||
this.db.get_master_connection(), this.tmp_dir, 1, null
|
||||
);
|
||||
|
||||
Geary.Db.Result result = this.db.query(
|
||||
"SELECT * FROM MessageAttachmentTable;"
|
||||
);
|
||||
assert_false(result.finished);
|
||||
assert_int(2, result.int_for("message_id"), "Unexpected message_id");
|
||||
assert_false(result.next(), "Attachment not deleted from db");
|
||||
|
||||
assert_false(attachments[0].file.query_exists(null),
|
||||
"Attachment not deleted from disk");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private GMime.Part new_part(string? mime_type,
|
||||
uint8[] body,
|
||||
GMime.ContentEncoding encoding = GMime.ContentEncoding.DEFAULT) {
|
||||
GMime.Part part = new GMime.Part();
|
||||
if (mime_type != null) {
|
||||
part.set_content_type(new GMime.ContentType.from_string(mime_type));
|
||||
}
|
||||
GMime.DataWrapper body_wrapper = new GMime.DataWrapper.with_stream(
|
||||
new GMime.StreamMem.with_buffer(body),
|
||||
GMime.ContentEncoding.DEFAULT
|
||||
);
|
||||
part.set_content_object(body_wrapper);
|
||||
part.encode(GMime.EncodingConstraint.7BIT);
|
||||
return part;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue