mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-17 06:07:13 -04:00
Fix attachment download base64 decoding issue (#1010)
This commit is contained in:
committed by
Leendert de Borst
parent
0dac97f4ff
commit
8ddefa56af
@@ -927,23 +927,7 @@ export class SqliteClient {
|
||||
for (const attachment of attachments) {
|
||||
const isExistingAttachment = originalAttachmentIds.includes(attachment.Id);
|
||||
|
||||
if (isExistingAttachment) {
|
||||
// Update existing attachment
|
||||
const updateQuery = `
|
||||
UPDATE Attachments
|
||||
SET Filename = ?,
|
||||
Blob = ?,
|
||||
UpdatedAt = ?,
|
||||
IsDeleted = ?
|
||||
WHERE Id = ?`;
|
||||
this.executeUpdate(updateQuery, [
|
||||
attachment.Filename,
|
||||
attachment.Blob as Uint8Array,
|
||||
currentDateTime,
|
||||
attachment.IsDeleted ? 1 : 0,
|
||||
attachment.Id
|
||||
]);
|
||||
} else {
|
||||
if (!isExistingAttachment) {
|
||||
// Insert new attachment
|
||||
const insertQuery = `
|
||||
INSERT INTO Attachments (Id, Filename, Blob, CredentialId, CreatedAt, UpdatedAt, IsDeleted)
|
||||
|
||||
@@ -9,7 +9,7 @@ import { StyleSheet, View, TouchableOpacity, Alert, Keyboard, KeyboardAvoidingVi
|
||||
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
|
||||
import Toast from 'react-native-toast-message';
|
||||
|
||||
import { CreateIdentityGenerator, IdentityHelperUtils, IdentityGenerator } from '@/utils/dist/shared/identity-generator';
|
||||
import { CreateIdentityGenerator, IdentityGenerator, IdentityHelperUtils } from '@/utils/dist/shared/identity-generator';
|
||||
import type { Attachment, Credential } from '@/utils/dist/shared/models/vault';
|
||||
import type { FaviconExtractModel } from '@/utils/dist/shared/models/webapi';
|
||||
import { CreatePasswordGenerator, PasswordGenerator } from '@/utils/dist/shared/password-generator';
|
||||
@@ -145,7 +145,7 @@ export default function AddEditCredentialScreen() : React.ReactNode {
|
||||
|
||||
/**
|
||||
* Initialize the identity and password generators with settings from user's vault.
|
||||
* @returns {identityGenerator: IdentityGenerator, passwordGenerator: PasswordGenerator}
|
||||
* @returns {identityGenerator: IIdentityGenerator, passwordGenerator: PasswordGenerator}
|
||||
*/
|
||||
const initializeGenerators = useCallback(async () : Promise<{ identityGenerator: IdentityGenerator, passwordGenerator: PasswordGenerator }> => {
|
||||
// Get default identity language from database
|
||||
@@ -724,7 +724,6 @@ export default function AddEditCredentialScreen() : React.ReactNode {
|
||||
<AttachmentUploader
|
||||
attachments={attachments}
|
||||
onAttachmentsChange={setAttachments}
|
||||
originalAttachmentIds={originalAttachmentIds}
|
||||
/>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import * as FileSystem from 'expo-file-system';
|
||||
import * as Sharing from 'expo-sharing';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { View, StyleSheet, TouchableOpacity, Alert } from 'react-native';
|
||||
import { View, StyleSheet, TouchableOpacity, Alert, Share } from 'react-native';
|
||||
|
||||
import type { Credential, Attachment } from '@/utils/dist/shared/models/vault';
|
||||
import emitter from '@/utils/EventEmitter';
|
||||
@@ -32,42 +33,38 @@ export const AttachmentSection: React.FC<AttachmentSectionProps> = ({ credential
|
||||
*/
|
||||
const downloadAttachment = async (attachment: Attachment): Promise<void> => {
|
||||
try {
|
||||
// Convert Uint8Array or number[] to Uint8Array
|
||||
const byteArray = attachment.Blob instanceof Uint8Array
|
||||
? attachment.Blob
|
||||
: new Uint8Array(attachment.Blob);
|
||||
const tempFile = `${FileSystem.cacheDirectory}${attachment.Filename}`;
|
||||
|
||||
const fileUri = `${FileSystem.documentDirectory}${attachment.Filename}`;
|
||||
// Step 1: Create a Blob
|
||||
if (typeof attachment.Blob === 'string') {
|
||||
// If attachment.Blob is already a base64 string
|
||||
const base64Data = attachment.Blob;
|
||||
|
||||
// Convert byte array to base64
|
||||
let binary = '';
|
||||
const bytes = new Uint8Array(byteArray);
|
||||
const len = bytes.byteLength;
|
||||
for (let i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
await FileSystem.writeAsStringAsync(tempFile, base64Data, {
|
||||
encoding: FileSystem.EncodingType.Base64,
|
||||
});
|
||||
}
|
||||
else {
|
||||
// Convert attachment.Blob to base64
|
||||
const base64Data = Buffer.from(attachment.Blob as unknown as string, 'base64');
|
||||
await FileSystem.writeAsStringAsync(tempFile, base64Data.toString(), {
|
||||
encoding: FileSystem.EncodingType.Base64,
|
||||
});
|
||||
}
|
||||
const base64 = btoa(binary);
|
||||
|
||||
// Write file to local storage
|
||||
await FileSystem.writeAsStringAsync(fileUri, base64, {
|
||||
encoding: FileSystem.EncodingType.Base64,
|
||||
// Step 2: Share using Sharing API (better handles mime types)
|
||||
await Share.share({
|
||||
url: tempFile,
|
||||
title: attachment.Filename,
|
||||
});
|
||||
|
||||
// Share the file
|
||||
if (await Sharing.isAvailableAsync()) {
|
||||
await Sharing.shareAsync(fileUri, {
|
||||
mimeType: 'application/octet-stream',
|
||||
dialogTitle: attachment.Filename,
|
||||
});
|
||||
} else {
|
||||
Alert.alert('Download Complete', `File saved to: ${fileUri}`);
|
||||
}
|
||||
// Optional cleanup
|
||||
await FileSystem.deleteAsync(tempFile);
|
||||
} catch (error) {
|
||||
console.error('Error downloading attachment:', error);
|
||||
Alert.alert('Error', 'Failed to download attachment');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Load the attachments.
|
||||
*/
|
||||
@@ -87,7 +84,6 @@ export const AttachmentSection: React.FC<AttachmentSectionProps> = ({ credential
|
||||
useEffect((): (() => void) => {
|
||||
loadAttachments();
|
||||
|
||||
// Add listener for credential changes to reload attachments
|
||||
const credentialChangedSub = emitter.addListener('credentialChanged', async (changedId: string) => {
|
||||
if (changedId === credential.Id) {
|
||||
await loadAttachments();
|
||||
@@ -163,4 +159,4 @@ export const AttachmentSection: React.FC<AttachmentSectionProps> = ({ credential
|
||||
))}
|
||||
</ThemedView>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import * as DocumentPicker from 'expo-document-picker';
|
||||
import * as FileSystem from 'expo-file-system';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { View, StyleSheet, TouchableOpacity, Alert } from 'react-native';
|
||||
@@ -34,7 +35,7 @@ export const AttachmentUploader: React.FC<AttachmentUploaderProps> = ({
|
||||
try {
|
||||
const result = await DocumentPicker.getDocumentAsync({
|
||||
multiple: true,
|
||||
copyToCacheDirectory: false,
|
||||
copyToCacheDirectory: true,
|
||||
});
|
||||
|
||||
if (result.canceled) {
|
||||
@@ -45,22 +46,59 @@ export const AttachmentUploader: React.FC<AttachmentUploaderProps> = ({
|
||||
|
||||
for (const file of result.assets) {
|
||||
if (file.uri) {
|
||||
// Read file as bytes
|
||||
const response = await fetch(file.uri);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const byteArray = new Uint8Array(arrayBuffer);
|
||||
try {
|
||||
let fileUri = file.uri;
|
||||
|
||||
const attachment: Attachment = {
|
||||
Id: crypto.randomUUID(),
|
||||
Filename: file.name,
|
||||
Blob: byteArray,
|
||||
CredentialId: '', // Will be set when saving credential
|
||||
CreatedAt: new Date().toISOString(),
|
||||
UpdatedAt: new Date().toISOString(),
|
||||
IsDeleted: false,
|
||||
};
|
||||
/*
|
||||
* If the URI is a content:// URI and copyToCacheDirectory didn't work,
|
||||
* try to copy it to a readable location
|
||||
*/
|
||||
if (fileUri.startsWith('content://')) {
|
||||
const tempUri = `${FileSystem.cacheDirectory}${file.name}`;
|
||||
await FileSystem.copyAsync({
|
||||
from: fileUri,
|
||||
to: tempUri,
|
||||
});
|
||||
fileUri = tempUri;
|
||||
}
|
||||
|
||||
newAttachments.push(attachment);
|
||||
// Read file as base64 string using FileSystem
|
||||
const base64Data = await FileSystem.readAsStringAsync(fileUri, {
|
||||
encoding: FileSystem.EncodingType.Base64,
|
||||
});
|
||||
|
||||
// Convert base64 to Uint8Array
|
||||
const binaryString = atob(base64Data);
|
||||
const byteArray = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
byteArray[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
|
||||
const attachment: Attachment = {
|
||||
Id: crypto.randomUUID(),
|
||||
Filename: file.name,
|
||||
Blob: byteArray,
|
||||
CredentialId: '', // Will be set when saving credential
|
||||
CreatedAt: new Date().toISOString(),
|
||||
UpdatedAt: new Date().toISOString(),
|
||||
IsDeleted: false,
|
||||
};
|
||||
|
||||
newAttachments.push(attachment);
|
||||
|
||||
// Clean up temporary file if we created one
|
||||
if (fileUri !== file.uri) {
|
||||
try {
|
||||
await FileSystem.deleteAsync(fileUri);
|
||||
} catch (cleanupError) {
|
||||
console.warn('Failed to cleanup temporary file:', cleanupError);
|
||||
}
|
||||
}
|
||||
} catch (fileError) {
|
||||
console.error('Error reading file:', fileError);
|
||||
setStatusMessage(`Error reading file ${file.name}.`);
|
||||
setTimeout(() => setStatusMessage(''), 3000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -222,7 +222,7 @@ PODS:
|
||||
- ExpoModulesCore
|
||||
- ExpoClipboard (7.0.1):
|
||||
- ExpoModulesCore
|
||||
- ExpoDocumentPicker (13.1.6):
|
||||
- ExpoDocumentPicker (13.0.3):
|
||||
- ExpoModulesCore
|
||||
- ExpoFileSystem (18.0.12):
|
||||
- ExpoModulesCore
|
||||
@@ -2533,7 +2533,7 @@ SPEC CHECKSUMS:
|
||||
ExpoAsset: 48386d40d53a8c1738929b3ed509bcad595b5516
|
||||
ExpoBlur: 392c1207f71d0ecf22371621c1fbd44ba84d9742
|
||||
ExpoClipboard: 44fd1c8959ee8f6175d059dc011b154c9709a969
|
||||
ExpoDocumentPicker: b263a279685b6640b8c8bc70d71c83067aeaae55
|
||||
ExpoDocumentPicker: 6d3d499cf15b692688a804f42927d0f35de5ebaa
|
||||
ExpoFileSystem: 42d363d3b96f9afab980dcef60d5657a4443c655
|
||||
ExpoFont: f354e926f8feae5e831ec8087f36652b44a0b188
|
||||
ExpoHaptics: 8d199b2f33245ea85289ff6c954c7ee7c00a5b5d
|
||||
|
||||
8
apps/mobile-app/package-lock.json
generated
8
apps/mobile-app/package-lock.json
generated
@@ -19,7 +19,7 @@
|
||||
"expo-clipboard": "~7.0.1",
|
||||
"expo-constants": "~17.0.8",
|
||||
"expo-dev-client": "~5.0.19",
|
||||
"expo-document-picker": "^13.1.6",
|
||||
"expo-document-picker": "~13.0.3",
|
||||
"expo-file-system": "~18.0.12",
|
||||
"expo-font": "~13.0.4",
|
||||
"expo-haptics": "~14.0.1",
|
||||
@@ -11150,9 +11150,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/expo-document-picker": {
|
||||
"version": "13.1.6",
|
||||
"resolved": "https://registry.npmjs.org/expo-document-picker/-/expo-document-picker-13.1.6.tgz",
|
||||
"integrity": "sha512-8FTQPDOkyCvFN/i4xyqzH7ELW4AsB6B3XBZQjn1FEdqpozo6rpNJRr7sWFU/93WrLgA9FJEKpKbyr6XxczK6BA==",
|
||||
"version": "13.0.3",
|
||||
"resolved": "https://registry.npmjs.org/expo-document-picker/-/expo-document-picker-13.0.3.tgz",
|
||||
"integrity": "sha512-348xcsiA/YhgWm1SuJNNdb5cUDpRJYCyIk8MhOU2MEDxbVRR+Q1TiUBTCIMVqaWHcxsFQzP56Wwv9n24qjeILg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"expo": "*"
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"expo-clipboard": "~7.0.1",
|
||||
"expo-constants": "~17.0.8",
|
||||
"expo-dev-client": "~5.0.19",
|
||||
"expo-document-picker": "^13.1.6",
|
||||
"expo-document-picker": "~13.0.3",
|
||||
"expo-file-system": "~18.0.12",
|
||||
"expo-font": "~13.0.4",
|
||||
"expo-haptics": "~14.0.1",
|
||||
|
||||
@@ -978,21 +978,7 @@ class SqliteClient {
|
||||
for (const attachment of attachments) {
|
||||
const isExistingAttachment = originalAttachmentIds.includes(attachment.Id);
|
||||
|
||||
if (isExistingAttachment) {
|
||||
// Update existing attachment
|
||||
const updateQuery = `
|
||||
UPDATE Attachments
|
||||
SET Filename = ?,
|
||||
Blob = ?,
|
||||
UpdatedAt = ?
|
||||
WHERE Id = ?`;
|
||||
await this.executeUpdate(updateQuery, [
|
||||
attachment.Filename,
|
||||
attachment.Blob as Uint8Array,
|
||||
currentDateTime,
|
||||
attachment.Id
|
||||
]);
|
||||
} else {
|
||||
if (!isExistingAttachment) {
|
||||
// Insert new attachment
|
||||
const insertQuery = `
|
||||
INSERT INTO Attachments (Id, Filename, Blob, CredentialId, CreatedAt, UpdatedAt, IsDeleted)
|
||||
|
||||
Reference in New Issue
Block a user