Skip to content

Commit 3330e23

Browse files
authored
fixes and tests for #51 (#52)
fixes #46 #50
1 parent 21145d4 commit 3330e23

File tree

3 files changed

+109
-29
lines changed

3 files changed

+109
-29
lines changed

src/client.jl

Lines changed: 56 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -602,48 +602,75 @@ is_request_interrupted(ex::InvocationException) = ex.reason == "request was inte
602602

603603

604604
"""
605-
deserialize_file(api_call::Function;
606-
folder_path::String=pwd(),
605+
storefile(api_call::Function;
606+
folder::AbstractString = pwd(),
607607
rename_file::String="",
608-
overwrite::Bool=true
609608
)::Tuple{Any,ApiResponse,String}
610609
611-
Saves response body into a file in a temporary folder,
612-
using the filename from the `Content-Disposition` header if provided.
613-
- `api_call`: API function that return `(result, http_response)` Tuple.
614-
- `folder_path`: file save location, default value is `pwd()``.
615-
- `rename_file`: rename the file, default value is `""`.
616-
- return: (result, http_response, file_path).
610+
Helper method that stores the result of an API call that returns file
611+
contents (as binary or text string) into a file.
612+
613+
Convenient to use it in a do block. Returns the path where file is stored additionally.
614+
615+
E.g.:
616+
```
617+
_result, _http_response, file = OpenAPI.Clients.storefile() do
618+
# Invoke the OpenaPI method that returns file contents.
619+
# This is the method that returns a tuple of (result, http_response).
620+
# The result is the file contents as binary or text string.
621+
fetch_file(api, "reports", "category1")
622+
end
623+
```
624+
625+
Parameters:
626+
627+
- `api_call`: The OpenAPI function call that returns file contents (as binary or text string). See example in method description.
628+
- `folder`: Location to store file, defaults to `pwd()`.
629+
- `filename`: Use this filename, overrides any filename that may be there in the `Content-Disposition` header.
630+
631+
Returns: (result, http_response, file_path)
617632
"""
618-
function deserialize_file(api_call::Function;
619-
folder_path::String=pwd(),
620-
rename_file::String="",
633+
function storefile(api_call::Function;
634+
folder::AbstractString = pwd(),
635+
filename::Union{String,Nothing} = nothing,
621636
)::Tuple{Any,ApiResponse,String}
622637

623638
result, http_response = api_call()
624639

625-
content_disposition_str = OpenAPI.Clients.header(http_response.raw,"content-disposition","")
626-
content_type_str = extract_filename(OpenAPI.Clients.header(http_response.raw,"content-type",""))
627-
628-
file_name = if !isempty(rename_file)
629-
rename_file
630-
elseif !isempty(content_disposition_str)
631-
content_disposition_str
632-
else
633-
"response"*extension_from_mime(MIME(content_type_str))
640+
if isnothing(filename)
641+
filename = extract_filename(http_response)
634642
end
635643

636-
file_path = joinpath(mkpath(folder_path),file_name)
637-
open(file_path,"w") do file
638-
write(file,result)
644+
mkpath(folder)
645+
filepath = joinpath(folder, filename)
646+
647+
open(filepath, "w") do io
648+
write(io, result)
639649
end
640-
return result, http_response, file_path
650+
651+
return result, http_response, filepath
641652
end
642653

643-
# extract_filename from content-disposition
644-
function extract_filename(str::String)::String
645-
m = match(r"filename=\"(.*?)\"",str)
646-
return isnothing(m) ? "" : m.captures[1]
654+
const content_disposition_re = r"filename\*?=['\"]?(?:UTF-\d['\"]*)?([^;\r\n\"']*)['\"]?;?"
655+
656+
"""
657+
extract_filename(resp::Downloads.Response)::String
658+
659+
Extracts the filename from the `Content-Disposition` header of the HTTP response.
660+
If not found, then creates a filename from the `Content-Type` header.
661+
"""
662+
extract_filename(resp::ApiResponse) = extract_filename(resp.raw)
663+
function extract_filename(resp::Downloads.Response)::String
664+
# attempt to extract filename from content-disposition header
665+
content_disposition_str = header(resp, "content-disposition", "")
666+
m = match(content_disposition_re, content_disposition_str)
667+
if !isnothing(m) && !isempty(m.captures) && !isnothing(m.captures[1])
668+
return m.captures[1]
669+
end
670+
671+
# attempt to create a filename from content-type header
672+
content_type_str = header(resp, "content-type", "")
673+
return string("response", extension_from_mime(MIME(content_type_str)))
647674
end
648675

649676
end # module Clients

test/client/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ function runtests(; skip_petstore=false, test_file_upload=false)
1717
test_date()
1818
test_misc()
1919
test_has_property()
20+
test_storefile()
2021
end
2122
@testset "Validations" begin
2223
test_validations()

test/client/utilstests.jl

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ using Test
44
using Dates
55
using TimeZones
66
using Base64
7+
using Downloads
78

89
function test_date()
910
dt_now = now()
@@ -242,4 +243,55 @@ function test_misc()
242243
@test OpenAPI.from_json(Any, json) === json
243244
@test OpenAPI.from_json(String, json) == "{}"
244245
@test isa(OpenAPI.from_json(Dict{Any,Any}, json), Dict{Any,Any})
246+
end
247+
248+
const content_disposition_tests = [
249+
(content_disposition="attachment; filename=content.txt", content_type="", filename="content.txt"),
250+
(content_disposition="attachment; filename*=UTF-8''filename.txt", content_type="", filename="filename.txt"),
251+
(content_disposition="attachment; filename=\"Image File\"; filename*=utf-8''UTF8ImageFile", content_type="", filename="Image File"),
252+
(content_disposition="attachment; filename=\"चित्त.jpg\"", content_type="", filename="चित्त.jpg"),
253+
(content_disposition="", content_type="", filename="response"),
254+
(content_disposition="", content_type="image/jpg", filename="response"),
255+
]
256+
257+
function test_storefile()
258+
for test_data in content_disposition_tests
259+
headers = [
260+
"Content-Disposition" => test_data.content_disposition,
261+
"Content-Type" => test_data.content_type,
262+
]
263+
resp = Downloads.Response("GET", "http://test/", 200, "", headers)
264+
265+
@test OpenAPI.Clients.extract_filename(resp) == test_data.filename
266+
end
267+
268+
mktempdir() do tmpdir
269+
test_data = content_disposition_tests[1]
270+
headers = [
271+
"Content-Disposition" => test_data.content_disposition,
272+
"Content-Type" => test_data.content_type,
273+
]
274+
resp = OpenAPI.Clients.ApiResponse(Downloads.Response("GET", "http://test/", 200, "", headers))
275+
file_contents = "test file data"
276+
277+
# test extraction of filename from headers
278+
result, http_response, filepath = OpenAPI.Clients.storefile(; folder=tmpdir) do
279+
return file_contents, resp
280+
end
281+
282+
@test result == file_contents
283+
@test http_response == resp
284+
@test filepath == joinpath(tmpdir, test_data.filename)
285+
@test read(filepath, String) == file_contents
286+
287+
# test overriding filename
288+
result, http_response, filepath = OpenAPI.Clients.storefile(; folder=tmpdir, filename="overridename.txt") do
289+
return file_contents, resp
290+
end
291+
292+
@test result == file_contents
293+
@test http_response == resp
294+
@test filepath == joinpath(tmpdir, "overridename.txt")
295+
@test read(filepath, String) == file_contents
296+
end
245297
end

0 commit comments

Comments
 (0)