Add last modified tracking: show 'Last modified: date by username' in document viewer
- Added modified_by column to files table - Updated WOPI PutFile to track who modified the file - Updated view handlers to return file metadata (name, size, lastModified, modifiedByName) - Updated Flutter models and UI to display last modified info
This commit is contained in:
@@ -35,6 +35,7 @@ class DocumentViewerBloc
|
|||||||
viewUrl: session.viewUrl,
|
viewUrl: session.viewUrl,
|
||||||
caps: session.capabilities,
|
caps: session.capabilities,
|
||||||
token: session.token,
|
token: session.token,
|
||||||
|
fileInfo: session.fileInfo,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
_expiryTimer = Timer(
|
_expiryTimer = Timer(
|
||||||
|
|||||||
@@ -16,15 +16,17 @@ class DocumentViewerReady extends DocumentViewerState {
|
|||||||
final Uri viewUrl;
|
final Uri viewUrl;
|
||||||
final DocumentCapabilities caps;
|
final DocumentCapabilities caps;
|
||||||
final String token;
|
final String token;
|
||||||
|
final FileInfo? fileInfo;
|
||||||
|
|
||||||
const DocumentViewerReady({
|
const DocumentViewerReady({
|
||||||
required this.viewUrl,
|
required this.viewUrl,
|
||||||
required this.caps,
|
required this.caps,
|
||||||
required this.token,
|
required this.token,
|
||||||
|
this.fileInfo,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [viewUrl, caps, token];
|
List<Object> get props => [viewUrl, caps, token, if (fileInfo != null) fileInfo!];
|
||||||
}
|
}
|
||||||
|
|
||||||
class DocumentViewerError extends DocumentViewerState {
|
class DocumentViewerError extends DocumentViewerState {
|
||||||
|
|||||||
@@ -1,5 +1,33 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
class FileInfo extends Equatable {
|
||||||
|
final String name;
|
||||||
|
final int size;
|
||||||
|
final DateTime? lastModified;
|
||||||
|
final String? modifiedByName;
|
||||||
|
|
||||||
|
const FileInfo({
|
||||||
|
required this.name,
|
||||||
|
required this.size,
|
||||||
|
this.lastModified,
|
||||||
|
this.modifiedByName,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [name, size, lastModified, modifiedByName];
|
||||||
|
|
||||||
|
factory FileInfo.fromJson(Map<String, dynamic> json) {
|
||||||
|
return FileInfo(
|
||||||
|
name: json['name'] ?? '',
|
||||||
|
size: json['size'] ?? 0,
|
||||||
|
lastModified: json['lastModified'] != null
|
||||||
|
? DateTime.tryParse(json['lastModified'])
|
||||||
|
: null,
|
||||||
|
modifiedByName: json['modifiedByName'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class DocumentCapabilities extends Equatable {
|
class DocumentCapabilities extends Equatable {
|
||||||
final bool canEdit;
|
final bool canEdit;
|
||||||
final bool canAnnotate;
|
final bool canAnnotate;
|
||||||
|
|||||||
@@ -6,16 +6,18 @@ class ViewerSession extends Equatable {
|
|||||||
final DocumentCapabilities capabilities;
|
final DocumentCapabilities capabilities;
|
||||||
final String token;
|
final String token;
|
||||||
final DateTime expiresAt;
|
final DateTime expiresAt;
|
||||||
|
final FileInfo? fileInfo;
|
||||||
|
|
||||||
const ViewerSession({
|
const ViewerSession({
|
||||||
required this.viewUrl,
|
required this.viewUrl,
|
||||||
required this.capabilities,
|
required this.capabilities,
|
||||||
required this.token,
|
required this.token,
|
||||||
required this.expiresAt,
|
required this.expiresAt,
|
||||||
|
this.fileInfo,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [viewUrl, capabilities, token, expiresAt];
|
List<Object?> get props => [viewUrl, capabilities, token, expiresAt, fileInfo];
|
||||||
|
|
||||||
factory ViewerSession.fromJson(Map<String, dynamic> json) {
|
factory ViewerSession.fromJson(Map<String, dynamic> json) {
|
||||||
return ViewerSession(
|
return ViewerSession(
|
||||||
@@ -23,6 +25,7 @@ class ViewerSession extends Equatable {
|
|||||||
capabilities: DocumentCapabilities.fromJson(json['capabilities']),
|
capabilities: DocumentCapabilities.fromJson(json['capabilities']),
|
||||||
token: json['token'],
|
token: json['token'],
|
||||||
expiresAt: DateTime.parse(json['expiresAt']),
|
expiresAt: DateTime.parse(json['expiresAt']),
|
||||||
|
fileInfo: json['fileInfo'] != null ? FileInfo.fromJson(json['fileInfo']) : null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,6 +112,20 @@ class _DocumentViewerModalState extends State<DocumentViewerModal> {
|
|||||||
BlocBuilder<DocumentViewerBloc, DocumentViewerState>(
|
BlocBuilder<DocumentViewerBloc, DocumentViewerState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state is DocumentViewerReady) {
|
if (state is DocumentViewerReady) {
|
||||||
|
final fileInfo = state.fileInfo;
|
||||||
|
String lastModifiedText = 'Last modified: Unknown';
|
||||||
|
if (fileInfo != null) {
|
||||||
|
final modifiedDate = fileInfo.lastModified;
|
||||||
|
final modifiedBy = fileInfo.modifiedByName;
|
||||||
|
if (modifiedDate != null) {
|
||||||
|
final formattedDate = '${modifiedDate.day.toString().padLeft(2, '0')}.${modifiedDate.month.toString().padLeft(2, '0')}.${modifiedDate.year} ${modifiedDate.hour.toString().padLeft(2, '0')}:${modifiedDate.minute.toString().padLeft(2, '0')}';
|
||||||
|
if (modifiedBy != null && modifiedBy.isNotEmpty) {
|
||||||
|
lastModifiedText = 'Last modified: $formattedDate by $modifiedBy';
|
||||||
|
} else {
|
||||||
|
lastModifiedText = 'Last modified: $formattedDate';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return Container(
|
return Container(
|
||||||
height: 30,
|
height: 30,
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
@@ -119,9 +133,9 @@ class _DocumentViewerModalState extends State<DocumentViewerModal> {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppTheme.primaryBackground.withValues(alpha: 0.3),
|
color: AppTheme.primaryBackground.withValues(alpha: 0.3),
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: Text(
|
||||||
'Last modified: Unknown by Unknown (v1)',
|
lastModifiedText,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: AppTheme.secondaryText,
|
color: AppTheme.secondaryText,
|
||||||
),
|
),
|
||||||
@@ -561,13 +575,26 @@ class _DocumentViewerState extends State<DocumentViewer> {
|
|||||||
child: BlocBuilder<DocumentViewerBloc, DocumentViewerState>(
|
child: BlocBuilder<DocumentViewerBloc, DocumentViewerState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state is DocumentViewerReady) {
|
if (state is DocumentViewerReady) {
|
||||||
// Placeholder for meta
|
final fileInfo = state.fileInfo;
|
||||||
|
String lastModifiedText = 'Last modified: Unknown';
|
||||||
|
if (fileInfo != null) {
|
||||||
|
final modifiedDate = fileInfo.lastModified;
|
||||||
|
final modifiedBy = fileInfo.modifiedByName;
|
||||||
|
if (modifiedDate != null) {
|
||||||
|
final formattedDate = '${modifiedDate.day.toString().padLeft(2, '0')}.${modifiedDate.month.toString().padLeft(2, '0')}.${modifiedDate.year} ${modifiedDate.hour.toString().padLeft(2, '0')}:${modifiedDate.minute.toString().padLeft(2, '0')}';
|
||||||
|
if (modifiedBy != null && modifiedBy.isNotEmpty) {
|
||||||
|
lastModifiedText = 'Last modified: $formattedDate by $modifiedBy';
|
||||||
|
} else {
|
||||||
|
lastModifiedText = 'Last modified: $formattedDate';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return Container(
|
return Container(
|
||||||
height: 30,
|
height: 30,
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Last modified: Unknown by Unknown (v1)',
|
lastModifiedText,
|
||||||
style: const TextStyle(fontSize: 12),
|
style: const TextStyle(fontSize: 12),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,293 +0,0 @@
|
|||||||
import '../models/file_item.dart';
|
|
||||||
import '../models/viewer_session.dart';
|
|
||||||
import '../models/editor_session.dart';
|
|
||||||
import '../models/annotation.dart';
|
|
||||||
import '../models/document_capabilities.dart';
|
|
||||||
import '../models/api_error.dart';
|
|
||||||
import '../repositories/file_repository.dart';
|
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
import 'package:uuid/uuid.dart';
|
|
||||||
|
|
||||||
class MockFileRepository implements FileRepository {
|
|
||||||
final Map<String, List<FileItem>> _orgFiles = {};
|
|
||||||
final _uuid = const Uuid();
|
|
||||||
|
|
||||||
List<FileItem> _getFilesForOrg(String orgId) {
|
|
||||||
if (!_orgFiles.containsKey(orgId)) {
|
|
||||||
// Initialize with different files per org
|
|
||||||
if (orgId == 'org1') {
|
|
||||||
_orgFiles[orgId] = [
|
|
||||||
FileItem(
|
|
||||||
id: _uuid.v4(),
|
|
||||||
name: 'Personal Documents',
|
|
||||||
path: '/Personal Documents',
|
|
||||||
type: FileType.folder,
|
|
||||||
lastModified: DateTime.now(),
|
|
||||||
),
|
|
||||||
FileItem(
|
|
||||||
id: _uuid.v4(),
|
|
||||||
name: 'Photos',
|
|
||||||
path: '/Photos',
|
|
||||||
type: FileType.folder,
|
|
||||||
lastModified: DateTime.now(),
|
|
||||||
),
|
|
||||||
FileItem(
|
|
||||||
id: _uuid.v4(),
|
|
||||||
name: 'resume.pdf',
|
|
||||||
path: '/resume.pdf',
|
|
||||||
type: FileType.file,
|
|
||||||
size: 1024,
|
|
||||||
lastModified: DateTime.now(),
|
|
||||||
),
|
|
||||||
FileItem(
|
|
||||||
id: _uuid.v4(),
|
|
||||||
name: 'notes.txt',
|
|
||||||
path: '/notes.txt',
|
|
||||||
type: FileType.file,
|
|
||||||
size: 256,
|
|
||||||
lastModified: DateTime.now(),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
} else if (orgId == 'org2') {
|
|
||||||
_orgFiles[orgId] = [
|
|
||||||
FileItem(
|
|
||||||
id: _uuid.v4(),
|
|
||||||
name: 'Company Reports',
|
|
||||||
path: '/Company Reports',
|
|
||||||
type: FileType.folder,
|
|
||||||
lastModified: DateTime.now(),
|
|
||||||
),
|
|
||||||
FileItem(
|
|
||||||
id: _uuid.v4(),
|
|
||||||
name: 'annual_report.pdf',
|
|
||||||
path: '/annual_report.pdf',
|
|
||||||
type: FileType.file,
|
|
||||||
size: 2048,
|
|
||||||
lastModified: DateTime.now(),
|
|
||||||
),
|
|
||||||
FileItem(
|
|
||||||
id: _uuid.v4(),
|
|
||||||
name: 'presentation.pptx',
|
|
||||||
path: '/presentation.pptx',
|
|
||||||
type: FileType.file,
|
|
||||||
size: 4096,
|
|
||||||
lastModified: DateTime.now(),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
} else if (orgId == 'org3') {
|
|
||||||
_orgFiles[orgId] = [
|
|
||||||
FileItem(
|
|
||||||
id: _uuid.v4(),
|
|
||||||
name: 'Project Code',
|
|
||||||
path: '/Project Code',
|
|
||||||
type: FileType.folder,
|
|
||||||
lastModified: DateTime.now(),
|
|
||||||
),
|
|
||||||
FileItem(
|
|
||||||
id: _uuid.v4(),
|
|
||||||
name: 'side_project.dart',
|
|
||||||
path: '/side_project.dart',
|
|
||||||
type: FileType.file,
|
|
||||||
size: 512,
|
|
||||||
lastModified: DateTime.now(),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
// Default for new orgs
|
|
||||||
_orgFiles[orgId] = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return _orgFiles[orgId]!;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<List<FileItem>> getFiles(String orgId, String path) async {
|
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
|
||||||
final files = _getFilesForOrg(orgId);
|
|
||||||
if (path == '/') {
|
|
||||||
return files.where((f) => !f.path.substring(1).contains('/')).toList();
|
|
||||||
} else {
|
|
||||||
return files
|
|
||||||
.where((f) => f.path.startsWith('$path/') && f.path != path)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<FileItem?> getFile(String orgId, String path) async {
|
|
||||||
final files = _getFilesForOrg(orgId);
|
|
||||||
final index = files.indexWhere((f) => f.path == path);
|
|
||||||
return index != -1 ? files[index] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<EditorSession> requestEditorSession(
|
|
||||||
String orgId,
|
|
||||||
String fileId,
|
|
||||||
) async {
|
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
|
||||||
// Mock: determine editability
|
|
||||||
final isEditable =
|
|
||||||
fileId.endsWith('.docx') ||
|
|
||||||
fileId.endsWith('.xlsx') ||
|
|
||||||
fileId.endsWith('.pptx');
|
|
||||||
final editUrl = Uri.parse(
|
|
||||||
'https://office.b0esche.cloud/editor/$orgId/$fileId?editable=$isEditable',
|
|
||||||
);
|
|
||||||
final expiresAt = DateTime.now().add(const Duration(minutes: 30));
|
|
||||||
return EditorSession(
|
|
||||||
editUrl: editUrl,
|
|
||||||
token: 'mock-editor-token',
|
|
||||||
readOnly: !isEditable,
|
|
||||||
expiresAt: expiresAt,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> deleteFile(String orgId, String path) async {
|
|
||||||
final files = _getFilesForOrg(orgId);
|
|
||||||
files.removeWhere((f) => f.path == path);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> createFolder(
|
|
||||||
String orgId,
|
|
||||||
String parentPath,
|
|
||||||
String folderName,
|
|
||||||
) async {
|
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
|
||||||
final normalizedName = folderName.startsWith('/')
|
|
||||||
? folderName.substring(1)
|
|
||||||
: folderName;
|
|
||||||
final newPath = parentPath == '/'
|
|
||||||
? '/$normalizedName'
|
|
||||||
: '$parentPath/$normalizedName';
|
|
||||||
final files = _getFilesForOrg(orgId);
|
|
||||||
files.add(
|
|
||||||
FileItem(
|
|
||||||
id: _uuid.v4(),
|
|
||||||
name: normalizedName,
|
|
||||||
path: newPath,
|
|
||||||
type: FileType.folder,
|
|
||||||
lastModified: DateTime.now(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> moveFile(
|
|
||||||
String orgId,
|
|
||||||
String sourcePath,
|
|
||||||
String targetPath,
|
|
||||||
) async {
|
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
|
||||||
final files = _getFilesForOrg(orgId);
|
|
||||||
final fileIndex = files.indexWhere((f) => f.path == sourcePath);
|
|
||||||
if (fileIndex != -1) {
|
|
||||||
final file = files[fileIndex];
|
|
||||||
final newName = file.path.split('/').last;
|
|
||||||
final newPath = targetPath == '/' ? '/$newName' : '$targetPath/$newName';
|
|
||||||
files[fileIndex] = FileItem(
|
|
||||||
id: file.id,
|
|
||||||
name: file.name,
|
|
||||||
path: newPath,
|
|
||||||
type: file.type,
|
|
||||||
size: file.size,
|
|
||||||
lastModified: DateTime.now(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> renameFile(String orgId, String path, String newName) async {
|
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
|
||||||
final files = _getFilesForOrg(orgId);
|
|
||||||
final fileIndex = files.indexWhere((f) => f.path == path);
|
|
||||||
if (fileIndex != -1) {
|
|
||||||
final file = files[fileIndex];
|
|
||||||
final parentPath = p.dirname(path);
|
|
||||||
final newPath = parentPath == '.' ? '/$newName' : '$parentPath/$newName';
|
|
||||||
files[fileIndex] = FileItem(
|
|
||||||
id: file.id,
|
|
||||||
name: newName,
|
|
||||||
path: newPath,
|
|
||||||
type: file.type,
|
|
||||||
size: file.size,
|
|
||||||
lastModified: DateTime.now(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<List<FileItem>> searchFiles(String orgId, String query) async {
|
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
|
||||||
final files = _getFilesForOrg(orgId);
|
|
||||||
return files
|
|
||||||
.where((f) => f.name.toLowerCase().contains(query.toLowerCase()))
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> uploadFile(String orgId, FileItem file) async {
|
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
|
||||||
final files = _getFilesForOrg(orgId);
|
|
||||||
files.add(
|
|
||||||
FileItem(
|
|
||||||
id: _uuid.v4(),
|
|
||||||
name: file.name,
|
|
||||||
path: file.path,
|
|
||||||
type: file.type,
|
|
||||||
size: file.size,
|
|
||||||
lastModified: file.lastModified,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<ViewerSession> requestViewerSession(
|
|
||||||
String orgId,
|
|
||||||
String fileId,
|
|
||||||
) async {
|
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
|
||||||
if (fileId.contains('forbidden')) {
|
|
||||||
throw ApiError(
|
|
||||||
code: 'permission_denied',
|
|
||||||
message: 'Access denied',
|
|
||||||
status: 403,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (fileId.contains('notfound')) {
|
|
||||||
throw ApiError(code: 'not_found', message: 'File not found', status: 404);
|
|
||||||
}
|
|
||||||
// Mock: assume fileId is path, determine if PDF
|
|
||||||
final isPdf = fileId.endsWith('.pdf');
|
|
||||||
final caps = DocumentCapabilities(
|
|
||||||
canEdit: !isPdf && (fileId.endsWith('.docx') || fileId.endsWith('.xlsx')),
|
|
||||||
canAnnotate: isPdf,
|
|
||||||
isPdf: isPdf,
|
|
||||||
mimeType: isPdf ? 'application/pdf' : 'application/octet-stream',
|
|
||||||
);
|
|
||||||
// Mock URL
|
|
||||||
final viewUrl = Uri.parse(
|
|
||||||
'https://office.b0esche.cloud/viewer/$orgId/$fileId',
|
|
||||||
);
|
|
||||||
final token = 'mock-viewer-token';
|
|
||||||
final expiresAt = DateTime.now().add(const Duration(minutes: 30));
|
|
||||||
return ViewerSession(
|
|
||||||
viewUrl: viewUrl,
|
|
||||||
capabilities: caps,
|
|
||||||
token: token,
|
|
||||||
expiresAt: expiresAt,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> saveAnnotations(
|
|
||||||
String orgId,
|
|
||||||
String fileId,
|
|
||||||
List<Annotation> annotations,
|
|
||||||
) async {
|
|
||||||
await Future.delayed(const Duration(seconds: 2));
|
|
||||||
// Mock: just delay, assume success
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -126,15 +126,17 @@ type Activity struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type File struct {
|
type File struct {
|
||||||
ID uuid.UUID
|
ID uuid.UUID
|
||||||
OrgID *uuid.UUID
|
OrgID *uuid.UUID
|
||||||
UserID *uuid.UUID
|
UserID *uuid.UUID
|
||||||
Name string
|
Name string
|
||||||
Path string
|
Path string
|
||||||
Type string
|
Type string
|
||||||
Size int64
|
Size int64
|
||||||
LastModified time.Time
|
LastModified time.Time
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
|
ModifiedBy *uuid.UUID
|
||||||
|
ModifiedByName string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) GetOrCreateUser(ctx context.Context, sub, email, name string) (*User, error) {
|
func (db *DB) GetOrCreateUser(ctx context.Context, sub, email, name string) (*User, error) {
|
||||||
@@ -465,12 +467,17 @@ func (db *DB) GetFileByID(ctx context.Context, fileID uuid.UUID) (*File, error)
|
|||||||
var f File
|
var f File
|
||||||
var orgNull sql.NullString
|
var orgNull sql.NullString
|
||||||
var userNull sql.NullString
|
var userNull sql.NullString
|
||||||
|
var modifiedByNull sql.NullString
|
||||||
|
var modifiedByNameNull sql.NullString
|
||||||
|
|
||||||
err := db.QueryRowContext(ctx, `
|
err := db.QueryRowContext(ctx, `
|
||||||
SELECT id, org_id::text, user_id::text, name, path, type, size, last_modified, created_at
|
SELECT f.id, f.org_id::text, f.user_id::text, f.name, f.path, f.type, f.size, f.last_modified, f.created_at,
|
||||||
FROM files
|
f.modified_by::text, u.display_name
|
||||||
WHERE id = $1
|
FROM files f
|
||||||
`, fileID).Scan(&f.ID, &orgNull, &userNull, &f.Name, &f.Path, &f.Type, &f.Size, &f.LastModified, &f.CreatedAt)
|
LEFT JOIN users u ON f.modified_by = u.id
|
||||||
|
WHERE f.id = $1
|
||||||
|
`, fileID).Scan(&f.ID, &orgNull, &userNull, &f.Name, &f.Path, &f.Type, &f.Size, &f.LastModified, &f.CreatedAt,
|
||||||
|
&modifiedByNull, &modifiedByNameNull)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -484,17 +491,24 @@ func (db *DB) GetFileByID(ctx context.Context, fileID uuid.UUID) (*File, error)
|
|||||||
uid, _ := uuid.Parse(userNull.String)
|
uid, _ := uuid.Parse(userNull.String)
|
||||||
f.UserID = &uid
|
f.UserID = &uid
|
||||||
}
|
}
|
||||||
|
if modifiedByNull.Valid {
|
||||||
|
mid, _ := uuid.Parse(modifiedByNull.String)
|
||||||
|
f.ModifiedBy = &mid
|
||||||
|
}
|
||||||
|
if modifiedByNameNull.Valid {
|
||||||
|
f.ModifiedByName = modifiedByNameNull.String
|
||||||
|
}
|
||||||
|
|
||||||
return &f, nil
|
return &f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateFileSize updates the size and modification time of a file
|
// UpdateFileSize updates the size, modification time, and modifier of a file
|
||||||
func (db *DB) UpdateFileSize(ctx context.Context, fileID uuid.UUID, size int64) error {
|
func (db *DB) UpdateFileSize(ctx context.Context, fileID uuid.UUID, size int64, modifiedBy *uuid.UUID) error {
|
||||||
_, err := db.ExecContext(ctx, `
|
_, err := db.ExecContext(ctx, `
|
||||||
UPDATE files
|
UPDATE files
|
||||||
SET size = $1, last_modified = NOW()
|
SET size = $1, last_modified = NOW(), modified_by = $3
|
||||||
WHERE id = $2
|
WHERE id = $2
|
||||||
`, size, fileID)
|
`, size, fileID, modifiedBy)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -520,6 +520,12 @@ func viewerHandler(w http.ResponseWriter, r *http.Request, db *database.DB, jwtM
|
|||||||
IsPdf bool `json:"isPdf"`
|
IsPdf bool `json:"isPdf"`
|
||||||
MimeType string `json:"mimeType"`
|
MimeType string `json:"mimeType"`
|
||||||
} `json:"capabilities"`
|
} `json:"capabilities"`
|
||||||
|
FileInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
LastModified string `json:"lastModified"`
|
||||||
|
ModifiedByName string `json:"modifiedByName"`
|
||||||
|
} `json:"fileInfo"`
|
||||||
ExpiresAt string `json:"expiresAt"`
|
ExpiresAt string `json:"expiresAt"`
|
||||||
}{
|
}{
|
||||||
ViewUrl: downloadPath,
|
ViewUrl: downloadPath,
|
||||||
@@ -530,6 +536,17 @@ func viewerHandler(w http.ResponseWriter, r *http.Request, db *database.DB, jwtM
|
|||||||
IsPdf bool `json:"isPdf"`
|
IsPdf bool `json:"isPdf"`
|
||||||
MimeType string `json:"mimeType"`
|
MimeType string `json:"mimeType"`
|
||||||
}{CanEdit: false, CanAnnotate: isPdf, IsPdf: isPdf, MimeType: mimeType},
|
}{CanEdit: false, CanAnnotate: isPdf, IsPdf: isPdf, MimeType: mimeType},
|
||||||
|
FileInfo: struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
LastModified string `json:"lastModified"`
|
||||||
|
ModifiedByName string `json:"modifiedByName"`
|
||||||
|
}{
|
||||||
|
Name: file.Name,
|
||||||
|
Size: file.Size,
|
||||||
|
LastModified: file.LastModified.UTC().Format(time.RFC3339),
|
||||||
|
ModifiedByName: file.ModifiedByName,
|
||||||
|
},
|
||||||
ExpiresAt: time.Now().Add(24 * time.Hour).UTC().Format(time.RFC3339),
|
ExpiresAt: time.Now().Add(24 * time.Hour).UTC().Format(time.RFC3339),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -606,6 +623,12 @@ func userViewerHandler(w http.ResponseWriter, r *http.Request, db *database.DB,
|
|||||||
IsPdf bool `json:"isPdf"`
|
IsPdf bool `json:"isPdf"`
|
||||||
MimeType string `json:"mimeType"`
|
MimeType string `json:"mimeType"`
|
||||||
} `json:"capabilities"`
|
} `json:"capabilities"`
|
||||||
|
FileInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
LastModified string `json:"lastModified"`
|
||||||
|
ModifiedByName string `json:"modifiedByName"`
|
||||||
|
} `json:"fileInfo"`
|
||||||
ExpiresAt string `json:"expiresAt"`
|
ExpiresAt string `json:"expiresAt"`
|
||||||
}{
|
}{
|
||||||
ViewUrl: downloadPath,
|
ViewUrl: downloadPath,
|
||||||
@@ -621,6 +644,17 @@ func userViewerHandler(w http.ResponseWriter, r *http.Request, db *database.DB,
|
|||||||
IsPdf: isPdf,
|
IsPdf: isPdf,
|
||||||
MimeType: mimeType,
|
MimeType: mimeType,
|
||||||
},
|
},
|
||||||
|
FileInfo: struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
LastModified string `json:"lastModified"`
|
||||||
|
ModifiedByName string `json:"modifiedByName"`
|
||||||
|
}{
|
||||||
|
Name: file.Name,
|
||||||
|
Size: file.Size,
|
||||||
|
LastModified: file.LastModified.UTC().Format(time.RFC3339),
|
||||||
|
ModifiedByName: file.ModifiedByName,
|
||||||
|
},
|
||||||
ExpiresAt: time.Now().Add(24 * time.Hour).UTC().Format(time.RFC3339),
|
ExpiresAt: time.Now().Add(24 * time.Hour).UTC().Format(time.RFC3339),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -512,7 +512,7 @@ func wopiPutFileHandler(w http.ResponseWriter, r *http.Request, db *database.DB,
|
|||||||
|
|
||||||
// Update file size and modification time in database
|
// Update file size and modification time in database
|
||||||
newSize := int64(len(content))
|
newSize := int64(len(content))
|
||||||
err = db.UpdateFileSize(r.Context(), fileUUID, newSize)
|
err = db.UpdateFileSize(r.Context(), fileUUID, newSize, &userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("[WOPI-STORAGE] Failed to update file size: file=%s error=%v\n", fileID, err)
|
fmt.Printf("[WOPI-STORAGE] Failed to update file size: file=%s error=%v\n", fileID, err)
|
||||||
// Don't fail the upload, just log the warning
|
// Don't fail the upload, just log the warning
|
||||||
|
|||||||
Reference in New Issue
Block a user