Download a file
The files of an object version are listed in its version information. To get the content, open a download session for one file version, read it in blocks and close the session.
Calls
- GetObjectInfoAndProperties
(or a search result):
object_info.version_info.fileslists the Files withtitle,extension,sizeandfile_ver. - DownloadFile_Begin with
obj_file_ver(the object'sobj_idand the file'sfile_ver),formatFILE_FORMAT_NATIVEandprepare_file_for_downloadingtrue. It returnsdownload_idandfile_outline.size. - DownloadFile_ReadBlock with
download_id,offsetandblock_sizeuntil you havesizebytes. The samples ask for 4 MiB, or for the rest of the file if that is less. - DownloadFile_Stop with
download_id, also after an error.
The recipe downloads every file of the latest version of a document (default ID 437) into a
folder (default mfiles-grpc-samples in the temporary folder).
Code
The function that downloads one file version:
Python
def download(objects, object_id: structs.ObjID, file_ver: structs.FileVer, path: Path) -> int:
"""Downloads one file version in blocks: DownloadFile_Begin opens a download session,
DownloadFile_ReadBlock reads it, DownloadFile_Stop closes it."""
begin = objects.DownloadFile_Begin(ops.DownloadFile_BeginRequest(
obj_file_ver=structs.ObjFileVer(obj_id=object_id, file_ver=file_ver),
format=enums.FILE_FORMAT_NATIVE,
prepare_file_for_downloading=True))
try:
size = begin.file_outline.size
with open(path, "wb") as output:
offset = 0
while offset < size:
block = objects.DownloadFile_ReadBlock(ops.DownloadFile_ReadBlockRequest(
download_id=begin.download_id,
block_size=min(BLOCK_SIZE, size - offset),
offset=offset))
if not block.data:
raise IOError(f"The server returned no data at offset {offset} of {size}.")
output.write(block.data)
offset += len(block.data)
return size
finally:
objects.DownloadFile_Stop(ops.DownloadFile_StopRequest(download_id=begin.download_id))
Source: python/recipes/download_file.py
C#
/// <summary>
/// Downloads one file version in blocks: DownloadFile_Begin opens a download session,
/// DownloadFile_ReadBlock reads it, DownloadFile_Stop closes it.
/// </summary>
private static async Task<long> DownloadAsync(IRPCObjectOperations.IRPCObjectOperationsClient objects,
ObjID objId, FileVer fileVer, string path)
{
var begin = await objects.DownloadFile_BeginAsync(new DownloadFile_BeginRequest
{
ObjFileVer = new ObjFileVer { ObjId = objId, FileVer = fileVer },
Format = FileFormat.Native,
PrepareFileForDownloading = true,
});
try
{
await using var output = System.IO.File.Create(path);
long size = begin.FileOutline.Size;
for (long offset = 0; offset < size;)
{
var block = await objects.DownloadFile_ReadBlockAsync(new DownloadFile_ReadBlockRequest
{
DownloadId = begin.DownloadId,
BlockSize = (uint)Math.Min(BlockSize, size - offset),
Offset = offset,
});
if (block.Data.IsEmpty)
throw new IOException($"The server returned no data at offset {offset} of {size}.");
block.Data.WriteTo(output);
offset += block.Data.Length;
}
return size;
}
finally
{
await objects.DownloadFile_StopAsync(new DownloadFile_StopRequest { DownloadId = begin.DownloadId });
}
}
Source: csharp/Recipes/DownloadFile.cs
Go
// download saves one file version in blocks: DownloadFile_Begin opens a download session,
// DownloadFile_ReadBlock reads it, DownloadFile_Stop closes it.
func download(ctx context.Context, objects mfiles.IRPCObjectOperationsClient, objID *mfiles.ObjID,
fileVer *mfiles.FileVer, path string) (int64, error) {
begin, err := objects.DownloadFile_Begin(ctx, &mfiles.DownloadFile_BeginRequest{
ObjFileVer: &mfiles.ObjFileVer{ObjId: objID, FileVer: fileVer},
Format: mfiles.FileFormat_FILE_FORMAT_NATIVE,
PrepareFileForDownloading: true,
})
if err != nil {
return 0, err
}
downloadID := begin.GetDownloadId()
defer objects.DownloadFile_Stop(context.WithoutCancel(ctx),
&mfiles.DownloadFile_StopRequest{DownloadId: downloadID})
output, err := os.Create(path)
if err != nil {
return 0, err
}
defer output.Close()
size := begin.GetFileOutline().GetSize()
for offset := int64(0); offset < size; {
block, err := objects.DownloadFile_ReadBlock(ctx, &mfiles.DownloadFile_ReadBlockRequest{
DownloadId: downloadID,
BlockSize: uint32(min(blockSize, size-offset)),
Offset: offset,
})
if err != nil {
return 0, err
}
if len(block.GetData()) == 0 {
return 0, fmt.Errorf("the server returned no data at offset %d of %d", offset, size)
}
if _, err := output.Write(block.GetData()); err != nil {
return 0, err
}
offset += int64(len(block.GetData()))
}
return size, output.Close()
}
Source: go/recipes/download_file/main.go
Output:
Proposal 7732 - ESTT Corporation (IT) (ID 437, version 1, modified 2012-06-07 10:39:43 UTC)
Downloaded Proposal 7732 - ESTT Corporation (IT).doc (57344 bytes) to C:\Users\me\AppData\Local\Temp\mfiles-grpc-samples\Proposal 7732 - ESTT Corporation (IT).doc
Pitfalls
prepare_file_for_downloadingmust be true. Without it,DownloadFile_Beginonly checks access and returnsdownload_id0, andDownloadFile_ReadBlockthen fails with0x8004000B"Not found. (The specified download session 0 does not exist.)".- At most 4 MiB per block. A larger
block_sizefails withE_INVALIDARG(0x80070057, "Block size exceeded the limit of 4194304 bytes."). The samples read blocks of at most 4 MiB; their helpers accept responses of any size, so the 4 MiB default of gRPC libraries does not get in the way. See Files. - The session can end with the last block. After a read that reaches the end of the file
with a block larger than the rest, the session may already be gone.
DownloadFile_Stopsucceeds on a closed session too, so always call it, also after errors. - The session lives on one server instance. On M-Files Cloud, keep the
mfilesmsmcookie so that every block goes to the instance that holds the session; the helpers do. - File names.
titlecan contain characters that are not allowed in file names, such as/. Replace them before you write the file, as the recipes do. - A document can have no files, or several (when the property Single file, 22, is false).
See also
- Files: download and upload in detail, formats and limits.
- Create a document for the upload direction.