Honor attachment filenames.

// FREEBIE
pull/1/head
Matthew Chen 8 years ago
parent bc10aea202
commit 40dcc7c873

@ -75,7 +75,8 @@ NS_ASSUME_NONNULL_BEGIN
key:attachmentProto.key key:attachmentProto.key
digest:digest digest:digest
contentType:attachmentProto.contentType contentType:attachmentProto.contentType
relay:relay]; relay:relay
filename:attachmentProto.fileName];
[attachmentIds addObject:pointer.uniqueId]; [attachmentIds addObject:pointer.uniqueId];

@ -21,9 +21,11 @@ typedef NS_ENUM(NSUInteger, TSAttachmentPointerState) {
key:(NSData *)key key:(NSData *)key
digest:(nullable NSData *)digest digest:(nullable NSData *)digest
contentType:(NSString *)contentType contentType:(NSString *)contentType
relay:(NSString *)relay NS_DESIGNATED_INITIALIZER; relay:(NSString *)relay
filename:(nullable NSString *)filename NS_DESIGNATED_INITIALIZER;
@property (nonatomic, readonly) NSString *relay; @property (nonatomic, readonly) NSString *relay;
@property (nonatomic, readonly, nullable) NSString *filename;
@property (atomic) TSAttachmentPointerState state; @property (atomic) TSAttachmentPointerState state;
// Though now required, `digest` may be null for pre-existing records or from // Though now required, `digest` may be null for pre-existing records or from

@ -30,6 +30,7 @@ NS_ASSUME_NONNULL_BEGIN
digest:(nullable NSData *)digest digest:(nullable NSData *)digest
contentType:(NSString *)contentType contentType:(NSString *)contentType
relay:(NSString *)relay relay:(NSString *)relay
filename:(nullable NSString *)filename
{ {
self = [super initWithServerId:serverId encryptionKey:key contentType:contentType]; self = [super initWithServerId:serverId encryptionKey:key contentType:contentType];
if (!self) { if (!self) {
@ -39,6 +40,7 @@ NS_ASSUME_NONNULL_BEGIN
_digest = digest; _digest = digest;
_state = TSAttachmentPointerStateEnqueued; _state = TSAttachmentPointerStateEnqueued;
_relay = relay; _relay = relay;
_filename = filename;
return self; return self;
} }

@ -24,6 +24,8 @@ NS_ASSUME_NONNULL_BEGIN
// This only applies for attachments being uploaded. // This only applies for attachments being uploaded.
@property (atomic) BOOL isUploaded; @property (atomic) BOOL isUploaded;
@property (nonatomic, readonly, nullable) NSString *filename;
#if TARGET_OS_IPHONE #if TARGET_OS_IPHONE
- (nullable UIImage *)image; - (nullable UIImage *)image;
#endif #endif

@ -42,6 +42,7 @@ NS_ASSUME_NONNULL_BEGIN
// state, but this constructor is used only for new incoming // state, but this constructor is used only for new incoming
// attachments which don't need to be uploaded. // attachments which don't need to be uploaded.
_isUploaded = YES; _isUploaded = YES;
_filename = pointer.filename;
return self; return self;
} }
@ -115,6 +116,7 @@ NS_ASSUME_NONNULL_BEGIN
{ {
return [MIMETypeUtil filePathForAttachment:self.uniqueId return [MIMETypeUtil filePathForAttachment:self.uniqueId
ofMIMEType:self.contentType ofMIMEType:self.contentType
filename:self.filename
inFolder:[[self class] attachmentsFolder]]; inFolder:[[self class] attachmentsFolder]];
} }

@ -1,4 +1,6 @@
// Copyright © 2016 Open Whisper Systems. All rights reserved. //
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
#import "OWSSyncContactsMessage.h" #import "OWSSyncContactsMessage.h"
#import "Contact.h" #import "Contact.h"
@ -39,7 +41,7 @@ NS_ASSUME_NONNULL_BEGIN
} }
OWSSignalServiceProtosAttachmentPointer *attachmentProto = OWSSignalServiceProtosAttachmentPointer *attachmentProto =
[self buildAttachmentProtoForAttachmentId:self.attachmentIds[0]]; [self buildAttachmentProtoForAttachmentId:self.attachmentIds[0] filename:nil];
OWSSignalServiceProtosSyncMessageContactsBuilder *contactsBuilder = OWSSignalServiceProtosSyncMessageContactsBuilder *contactsBuilder =
[OWSSignalServiceProtosSyncMessageContactsBuilder new]; [OWSSignalServiceProtosSyncMessageContactsBuilder new];

@ -1,4 +1,6 @@
// Copyright © 2016 Open Whisper Systems. All rights reserved. //
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
#import "OWSSyncGroupsMessage.h" #import "OWSSyncGroupsMessage.h"
#import "NSDate+millisecondTimeStamp.h" #import "NSDate+millisecondTimeStamp.h"
@ -25,7 +27,7 @@ NS_ASSUME_NONNULL_BEGIN
(unsigned long)self.attachmentIds.count); (unsigned long)self.attachmentIds.count);
} }
OWSSignalServiceProtosAttachmentPointer *attachmentProto = OWSSignalServiceProtosAttachmentPointer *attachmentProto =
[self buildAttachmentProtoForAttachmentId:self.attachmentIds[0]]; [self buildAttachmentProtoForAttachmentId:self.attachmentIds[0] filename:nil];
OWSSignalServiceProtosSyncMessageGroupsBuilder *groupsBuilder = OWSSignalServiceProtosSyncMessageGroupsBuilder *groupsBuilder =
[OWSSignalServiceProtosSyncMessageGroupsBuilder new]; [OWSSignalServiceProtosSyncMessageGroupsBuilder new];

@ -23,6 +23,7 @@ typedef NS_ENUM(NSInteger, TSGroupMetaMessage) {
@interface TSMessage : TSInteraction @interface TSMessage : TSInteraction
@property (nonatomic, readonly) NSMutableArray<NSString *> *attachmentIds; @property (nonatomic, readonly) NSMutableArray<NSString *> *attachmentIds;
// A map of attachment id-to-filename.
@property (nullable, nonatomic) NSString *body; @property (nullable, nonatomic) NSString *body;
@property (nonatomic) TSGroupMetaMessage groupMetaMessage; @property (nonatomic) TSGroupMetaMessage groupMetaMessage;
@property (nonatomic) uint32_t expiresInSeconds; @property (nonatomic) uint32_t expiresInSeconds;

@ -128,7 +128,6 @@ static const NSUInteger OWSMessageSchemaVersion = 3;
} }
if (!_attachmentIds) { if (!_attachmentIds) {
// previously allowed nil _attachmentIds
_attachmentIds = [NSMutableArray new]; _attachmentIds = [NSMutableArray new];
} }

@ -57,6 +57,7 @@ typedef NS_ENUM(NSInteger, TSOutgoingMessageState) {
@property BOOL hasSyncedTranscript; @property BOOL hasSyncedTranscript;
@property NSString *customMessage; @property NSString *customMessage;
@property (atomic, readonly) NSString *mostRecentFailureText; @property (atomic, readonly) NSString *mostRecentFailureText;
@property (nonatomic, readonly) NSMutableDictionary<NSString *, NSString *> *attachmentFilenameMap;
/** /**
* Whether the message should be serialized as a modern aka Content, or the old style legacy message. * Whether the message should be serialized as a modern aka Content, or the old style legacy message.
@ -94,10 +95,14 @@ typedef NS_ENUM(NSInteger, TSOutgoingMessageState) {
* @param attachmentId * @param attachmentId
* id of an AttachmentStream containing the meta data used when populating the attachment proto * id of an AttachmentStream containing the meta data used when populating the attachment proto
* *
* @param filename
* optional filename of the attachment.
*
* @return * @return
* An attachment pointer protobuf suitable for including in various container protobuf builders * An attachment pointer protobuf suitable for including in various container protobuf builders
*/ */
- (OWSSignalServiceProtosAttachmentPointer *)buildAttachmentProtoForAttachmentId:(NSString *)attachmentId; - (OWSSignalServiceProtosAttachmentPointer *)buildAttachmentProtoForAttachmentId:(NSString *)attachmentId
filename:(nullable NSString *)filename;
@end @end

@ -15,7 +15,13 @@ NS_ASSUME_NONNULL_BEGIN
- (instancetype)initWithCoder:(NSCoder *)coder - (instancetype)initWithCoder:(NSCoder *)coder
{ {
return [super initWithCoder:coder]; self = [super initWithCoder:coder];
if (self) {
if (!_attachmentFilenameMap) {
_attachmentFilenameMap = [NSMutableDictionary new];
}
}
return self;
} }
- (instancetype)initWithTimestamp:(uint64_t)timestamp - (instancetype)initWithTimestamp:(uint64_t)timestamp
@ -86,6 +92,7 @@ NS_ASSUME_NONNULL_BEGIN
} else { } else {
self.groupMetaMessage = TSGroupMessageNone; self.groupMetaMessage = TSGroupMessageNone;
} }
_attachmentFilenameMap = [NSMutableDictionary new];
OWSAssert(self.receivedAtDate); OWSAssert(self.receivedAtDate);
@ -142,7 +149,8 @@ NS_ASSUME_NONNULL_BEGIN
case TSGroupMessageNew: { case TSGroupMessageNew: {
if (gThread.groupModel.groupImage != nil && self.attachmentIds.count == 1) { if (gThread.groupModel.groupImage != nil && self.attachmentIds.count == 1) {
attachmentWasGroupAvatar = YES; attachmentWasGroupAvatar = YES;
[groupBuilder setAvatar:[self buildAttachmentProtoForAttachmentId:self.attachmentIds[0]]]; [groupBuilder
setAvatar:[self buildAttachmentProtoForAttachmentId:self.attachmentIds[0] filename:nil]];
} }
[groupBuilder setMembersArray:gThread.groupModel.groupMemberIds]; [groupBuilder setMembersArray:gThread.groupModel.groupMemberIds];
@ -160,7 +168,8 @@ NS_ASSUME_NONNULL_BEGIN
if (!attachmentWasGroupAvatar) { if (!attachmentWasGroupAvatar) {
NSMutableArray *attachments = [NSMutableArray new]; NSMutableArray *attachments = [NSMutableArray new];
for (NSString *attachmentId in self.attachmentIds) { for (NSString *attachmentId in self.attachmentIds) {
[attachments addObject:[self buildAttachmentProtoForAttachmentId:attachmentId]]; NSString *filename = self.attachmentFilenameMap[attachmentId];
[attachments addObject:[self buildAttachmentProtoForAttachmentId:attachmentId filename:filename]];
} }
[builder setAttachmentsArray:attachments]; [builder setAttachmentsArray:attachments];
} }
@ -191,7 +200,10 @@ NS_ASSUME_NONNULL_BEGIN
} }
- (OWSSignalServiceProtosAttachmentPointer *)buildAttachmentProtoForAttachmentId:(NSString *)attachmentId - (OWSSignalServiceProtosAttachmentPointer *)buildAttachmentProtoForAttachmentId:(NSString *)attachmentId
filename:(nullable NSString *)filename
{ {
OWSAssert(attachmentId.length > 0);
TSAttachment *attachment = [TSAttachmentStream fetchObjectWithUniqueID:attachmentId]; TSAttachment *attachment = [TSAttachmentStream fetchObjectWithUniqueID:attachmentId];
if (![attachment isKindOfClass:[TSAttachmentStream class]]) { if (![attachment isKindOfClass:[TSAttachmentStream class]]) {
DDLogError(@"Unexpected type for attachment builder: %@", attachment); DDLogError(@"Unexpected type for attachment builder: %@", attachment);
@ -202,6 +214,7 @@ NS_ASSUME_NONNULL_BEGIN
OWSSignalServiceProtosAttachmentPointerBuilder *builder = [OWSSignalServiceProtosAttachmentPointerBuilder new]; OWSSignalServiceProtosAttachmentPointerBuilder *builder = [OWSSignalServiceProtosAttachmentPointerBuilder new];
[builder setId:attachmentStream.serverId]; [builder setId:attachmentStream.serverId];
[builder setContentType:attachmentStream.contentType]; [builder setContentType:attachmentStream.contentType];
[builder setFileName:filename];
[builder setKey:attachmentStream.encryptionKey]; [builder setKey:attachmentStream.encryptionKey];
[builder setDigest:attachmentStream.digest]; [builder setDigest:attachmentStream.digest];

@ -55,6 +55,7 @@ NS_SWIFT_NAME(MessageSender)
*/ */
- (void)sendAttachmentData:(NSData *)attachmentData - (void)sendAttachmentData:(NSData *)attachmentData
contentType:(NSString *)contentType contentType:(NSString *)contentType
filename:(nullable NSString *)filename
inMessage:(TSOutgoingMessage *)outgoingMessage inMessage:(TSOutgoingMessage *)outgoingMessage
success:(void (^)())successHandler success:(void (^)())successHandler
failure:(void (^)(NSError *error))failureHandler; failure:(void (^)(NSError *error))failureHandler;

@ -437,6 +437,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
[self sendAttachmentData:attachmentData [self sendAttachmentData:attachmentData
contentType:contentType contentType:contentType
filename:nil
inMessage:message inMessage:message
success:successWithDeleteHandler success:successWithDeleteHandler
failure:failureWithDeleteHandler]; failure:failureWithDeleteHandler];
@ -444,6 +445,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
- (void)sendAttachmentData:(NSData *)data - (void)sendAttachmentData:(NSData *)data
contentType:(NSString *)contentType contentType:(NSString *)contentType
filename:(nullable NSString *)filename
inMessage:(TSOutgoingMessage *)message inMessage:(TSOutgoingMessage *)message
success:(void (^)())successHandler success:(void (^)())successHandler
failure:(void (^)(NSError *error))failureHandler failure:(void (^)(NSError *error))failureHandler
@ -467,6 +469,9 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
[attachmentStream save]; [attachmentStream save];
[message.attachmentIds addObject:attachmentStream.uniqueId]; [message.attachmentIds addObject:attachmentStream.uniqueId];
if (filename) {
message.attachmentFilenameMap[attachmentStream.uniqueId] = filename;
}
[message save]; [message save];
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{

@ -29,11 +29,11 @@ extern NSString *const OWSMimeTypeUnknownForTests;
+ (BOOL)isVideo:(NSString *)contentType; + (BOOL)isVideo:(NSString *)contentType;
+ (BOOL)isAudio:(NSString *)contentType; + (BOOL)isAudio:(NSString *)contentType;
+ (NSString *)filePathForAttachment:(NSString *)uniqueId ofMIMEType:(NSString *)contentType inFolder:(NSString *)folder; // filename is optional and should not be trusted.
+ (NSString *)filePathForImage:(NSString *)uniqueId ofMIMEType:(NSString *)contentType inFolder:(NSString *)folder; + (NSString *)filePathForAttachment:(NSString *)uniqueId
+ (NSString *)filePathForVideo:(NSString *)uniqueId ofMIMEType:(NSString *)contentType inFolder:(NSString *)folder; ofMIMEType:(NSString *)contentType
+ (NSString *)filePathForAudio:(NSString *)uniqueId ofMIMEType:(NSString *)contentType inFolder:(NSString *)folder; filename:(nullable NSString *)filename
+ (NSString *)filePathForAnimated:(NSString *)uniqueId ofMIMEType:(NSString *)contentType inFolder:(NSString *)folder; inFolder:(NSString *)folder;
+ (NSURL *)simLinkCorrectExtensionOfFile:(NSURL *)mediaURL ofMIMEType:(NSString *)contentType; + (NSURL *)simLinkCorrectExtensionOfFile:(NSURL *)mediaURL ofMIMEType:(NSString *)contentType;

@ -261,7 +261,65 @@ NSString *const OWSMimeTypeUnknownForTests = @"unknown/mimetype";
+ (NSString *)filePathForAttachment:(NSString *)uniqueId + (NSString *)filePathForAttachment:(NSString *)uniqueId
ofMIMEType:(NSString *)contentType ofMIMEType:(NSString *)contentType
inFolder:(NSString *)folder { filename:(nullable NSString *)filename
inFolder:(NSString *)folder
{
NSString *kDefaultFileExtension = @"bin";
if (filename.length > 0) {
NSString *normalizedFilename =
[filename stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
// Ensure that the filename is a valid filesystem name,
// replacing invalid characters with an underscore.
for (NSCharacterSet *invalidCharacterSet in @[
[NSCharacterSet whitespaceCharacterSet],
[NSCharacterSet newlineCharacterSet],
[NSCharacterSet illegalCharacterSet],
[NSCharacterSet controlCharacterSet],
[NSCharacterSet characterSetWithCharactersInString:@"<>|\\:()&;?*"],
]) {
normalizedFilename = [[normalizedFilename componentsSeparatedByCharactersInSet:invalidCharacterSet]
componentsJoinedByString:@"_"];
}
NSString *fileExtension = [[normalizedFilename pathExtension]
stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
NSString *filenameWithoutExtension = [[[normalizedFilename lastPathComponent] stringByDeletingPathExtension]
stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
// If the filename has not file extension, deduce one
// from the MIME type.
if (fileExtension.length < 1) {
fileExtension = [self fileExtensionForMIMEType:contentType];
if (fileExtension.length < 1) {
fileExtension = kDefaultFileExtension;
}
}
fileExtension = [fileExtension lowercaseString];
if (filenameWithoutExtension.length > 0) {
// Store the file in a subdirectory whose name is the uniqueId of this attachment,
// to avoid collisions between multiple attachments with the same name.
NSString *attachmentFolderPath = [folder stringByAppendingPathComponent:uniqueId];
NSError *error = nil;
BOOL attachmentFolderPathExists = [[NSFileManager defaultManager] fileExistsAtPath:attachmentFolderPath];
if (!attachmentFolderPathExists) {
[[NSFileManager defaultManager] createDirectoryAtPath:attachmentFolderPath
withIntermediateDirectories:YES
attributes:nil
error:&error];
if (error) {
DDLogError(@"Failed to create attachment directory: %@", error);
OWSAssert(0);
return nil;
}
}
return [attachmentFolderPath
stringByAppendingPathComponent:[NSString
stringWithFormat:@"%@.%@", filenameWithoutExtension, fileExtension]];
}
}
if ([self isVideo:contentType]) { if ([self isVideo:contentType]) {
return [MIMETypeUtil filePathForVideo:uniqueId ofMIMEType:contentType inFolder:folder]; return [MIMETypeUtil filePathForVideo:uniqueId ofMIMEType:contentType inFolder:folder];
} else if ([self isAudio:contentType]) { } else if ([self isAudio:contentType]) {
@ -291,7 +349,7 @@ NSString *const OWSMimeTypeUnknownForTests = @"unknown/mimetype";
DDLogError(@"Got asked for path of file %@ which is unsupported", contentType); DDLogError(@"Got asked for path of file %@ which is unsupported", contentType);
// Use a fallback file extension. // Use a fallback file extension.
return [self filePathForData:uniqueId withFileExtension:@"bin" inFolder:folder]; return [self filePathForData:uniqueId withFileExtension:kDefaultFileExtension inFolder:folder];
} }
+ (NSURL *)simLinkCorrectExtensionOfFile:(NSURL *)mediaURL ofMIMEType:(NSString *)contentType { + (NSURL *)simLinkCorrectExtensionOfFile:(NSURL *)mediaURL ofMIMEType:(NSString *)contentType {

Loading…
Cancel
Save