Skip to content

Commit d3662e4

Browse files
feat(ext/fetch): support fetching local files (#12545)
Closes #11925 Closes #2150 Co-authored-by: Bert Belder <[email protected]>
1 parent d080f1c commit d3662e4

File tree

6 files changed

+196
-10
lines changed

6 files changed

+196
-10
lines changed

cli/tests/unit/fetch_test.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ unitTest(
2323
unitTest({ permissions: { net: true } }, async function fetchProtocolError() {
2424
await assertRejects(
2525
async () => {
26-
await fetch("file:///");
26+
await fetch("ftp://localhost:21/a/file");
2727
},
2828
TypeError,
2929
"not supported",
@@ -1360,3 +1360,59 @@ unitTest(
13601360
client.close();
13611361
},
13621362
);
1363+
1364+
unitTest(async function fetchFilePerm() {
1365+
await assertRejects(async () => {
1366+
await fetch(new URL("../testdata/subdir/json_1.json", import.meta.url));
1367+
}, Deno.errors.PermissionDenied);
1368+
});
1369+
1370+
unitTest(async function fetchFilePermDoesNotExist() {
1371+
await assertRejects(async () => {
1372+
await fetch(new URL("./bad.json", import.meta.url));
1373+
}, Deno.errors.PermissionDenied);
1374+
});
1375+
1376+
unitTest(
1377+
{ permissions: { read: true } },
1378+
async function fetchFileBadMethod() {
1379+
await assertRejects(
1380+
async () => {
1381+
await fetch(
1382+
new URL("../testdata/subdir/json_1.json", import.meta.url),
1383+
{
1384+
method: "POST",
1385+
},
1386+
);
1387+
},
1388+
TypeError,
1389+
"Fetching files only supports the GET method. Received POST.",
1390+
);
1391+
},
1392+
);
1393+
1394+
unitTest(
1395+
{ permissions: { read: true } },
1396+
async function fetchFileDoesNotExist() {
1397+
await assertRejects(
1398+
async () => {
1399+
await fetch(new URL("./bad.json", import.meta.url));
1400+
},
1401+
TypeError,
1402+
);
1403+
},
1404+
);
1405+
1406+
unitTest(
1407+
{ permissions: { read: true } },
1408+
async function fetchFile() {
1409+
const res = await fetch(
1410+
new URL("../testdata/subdir/json_1.json", import.meta.url),
1411+
);
1412+
assert(res.ok);
1413+
const fixture = await Deno.readTextFile(
1414+
"cli/tests/testdata/subdir/json_1.json",
1415+
);
1416+
assertEquals(await res.text(), fixture);
1417+
},
1418+
);

ext/fetch/fs_fetch_handler.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
2+
3+
use crate::CancelHandle;
4+
use crate::CancelableResponseFuture;
5+
use crate::FetchHandler;
6+
use crate::FetchRequestBodyResource;
7+
8+
use deno_core::error::type_error;
9+
use deno_core::futures::FutureExt;
10+
use deno_core::futures::TryFutureExt;
11+
use deno_core::url::Url;
12+
use deno_core::CancelFuture;
13+
use reqwest::StatusCode;
14+
use std::rc::Rc;
15+
use tokio_util::io::ReaderStream;
16+
17+
/// An implementation which tries to read file URLs from the file system via
18+
/// tokio::fs.
19+
#[derive(Clone)]
20+
pub struct FsFetchHandler;
21+
22+
impl FetchHandler for FsFetchHandler {
23+
fn fetch_file(
24+
&mut self,
25+
url: Url,
26+
) -> (
27+
CancelableResponseFuture,
28+
Option<FetchRequestBodyResource>,
29+
Option<Rc<CancelHandle>>,
30+
) {
31+
let cancel_handle = CancelHandle::new_rc();
32+
let response_fut = async move {
33+
let path = url.to_file_path()?;
34+
let file = tokio::fs::File::open(path).map_err(|_| ()).await?;
35+
let stream = ReaderStream::new(file);
36+
let body = reqwest::Body::wrap_stream(stream);
37+
let response = http::Response::builder()
38+
.status(StatusCode::OK)
39+
.body(body)
40+
.map_err(|_| ())?
41+
.into();
42+
Ok::<_, ()>(response)
43+
}
44+
.map_err(move |_| {
45+
type_error("NetworkError when attempting to fetch resource.")
46+
})
47+
.or_cancel(&cancel_handle)
48+
.boxed_local();
49+
50+
(response_fut, None, Some(cancel_handle))
51+
}
52+
}

ext/fetch/lib.rs

Lines changed: 81 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
22

3+
mod fs_fetch_handler;
4+
35
use data_url::DataUrl;
46
use deno_core::error::type_error;
57
use deno_core::error::AnyError;
@@ -52,14 +54,21 @@ use tokio_util::io::StreamReader;
5254
pub use data_url;
5355
pub use reqwest;
5456

55-
pub fn init<P: FetchPermissions + 'static>(
57+
pub use fs_fetch_handler::FsFetchHandler;
58+
59+
pub fn init<FP, FH>(
5660
user_agent: String,
5761
root_cert_store: Option<RootCertStore>,
5862
proxy: Option<Proxy>,
5963
request_builder_hook: Option<fn(RequestBuilder) -> RequestBuilder>,
6064
unsafely_ignore_certificate_errors: Option<Vec<String>>,
6165
client_cert_chain_and_key: Option<(String, String)>,
62-
) -> Extension {
66+
file_fetch_handler: FH,
67+
) -> Extension
68+
where
69+
FP: FetchPermissions + 'static,
70+
FH: FetchHandler + 'static,
71+
{
6372
Extension::builder()
6473
.js(include_js_files!(
6574
prefix "deno:ext/fetch",
@@ -73,13 +82,13 @@ pub fn init<P: FetchPermissions + 'static>(
7382
"26_fetch.js",
7483
))
7584
.ops(vec![
76-
("op_fetch", op_sync(op_fetch::<P>)),
85+
("op_fetch", op_sync(op_fetch::<FP, FH>)),
7786
("op_fetch_send", op_async(op_fetch_send)),
7887
("op_fetch_request_write", op_async(op_fetch_request_write)),
7988
("op_fetch_response_read", op_async(op_fetch_response_read)),
8089
(
8190
"op_fetch_custom_client",
82-
op_sync(op_fetch_custom_client::<P>),
91+
op_sync(op_fetch_custom_client::<FP>),
8392
),
8493
])
8594
.state(move |state| {
@@ -103,6 +112,7 @@ pub fn init<P: FetchPermissions + 'static>(
103112
.clone(),
104113
client_cert_chain_and_key: client_cert_chain_and_key.clone(),
105114
});
115+
state.put::<FH>(file_fetch_handler.clone());
106116
Ok(())
107117
})
108118
.build()
@@ -117,6 +127,45 @@ pub struct HttpClientDefaults {
117127
pub client_cert_chain_and_key: Option<(String, String)>,
118128
}
119129

130+
pub type CancelableResponseFuture =
131+
Pin<Box<dyn Future<Output = CancelableResponseResult>>>;
132+
133+
pub trait FetchHandler: Clone {
134+
// Return the result of the fetch request consisting of a tuple of the
135+
// cancelable response result, the optional fetch body resource and the
136+
// optional cancel handle.
137+
fn fetch_file(
138+
&mut self,
139+
url: Url,
140+
) -> (
141+
CancelableResponseFuture,
142+
Option<FetchRequestBodyResource>,
143+
Option<Rc<CancelHandle>>,
144+
);
145+
}
146+
147+
/// A default implementation which will error for every request.
148+
#[derive(Clone)]
149+
pub struct DefaultFileFetchHandler;
150+
151+
impl FetchHandler for DefaultFileFetchHandler {
152+
fn fetch_file(
153+
&mut self,
154+
_url: Url,
155+
) -> (
156+
CancelableResponseFuture,
157+
Option<FetchRequestBodyResource>,
158+
Option<Rc<CancelHandle>>,
159+
) {
160+
let fut = async move {
161+
Ok(Err(type_error(
162+
"NetworkError when attempting to fetch resource.",
163+
)))
164+
};
165+
(Box::pin(fut), None, None)
166+
}
167+
}
168+
120169
pub trait FetchPermissions {
121170
fn check_net_url(&mut self, _url: &Url) -> Result<(), AnyError>;
122171
fn check_read(&mut self, _p: &Path) -> Result<(), AnyError>;
@@ -145,13 +194,14 @@ pub struct FetchReturn {
145194
cancel_handle_rid: Option<ResourceId>,
146195
}
147196

148-
pub fn op_fetch<FP>(
197+
pub fn op_fetch<FP, FH>(
149198
state: &mut OpState,
150199
args: FetchArgs,
151200
data: Option<ZeroCopyBuf>,
152201
) -> Result<FetchReturn, AnyError>
153202
where
154203
FP: FetchPermissions + 'static,
204+
FH: FetchHandler + 'static,
155205
{
156206
let client = if let Some(rid) = args.client_rid {
157207
let r = state.resource_table.get::<HttpClientResource>(rid)?;
@@ -167,6 +217,31 @@ where
167217
// Check scheme before asking for net permission
168218
let scheme = url.scheme();
169219
let (request_rid, request_body_rid, cancel_handle_rid) = match scheme {
220+
"file" => {
221+
let path = url.to_file_path().map_err(|_| {
222+
type_error("NetworkError when attempting to fetch resource.")
223+
})?;
224+
let permissions = state.borrow_mut::<FP>();
225+
permissions.check_read(&path)?;
226+
227+
if method != Method::GET {
228+
return Err(type_error(format!(
229+
"Fetching files only supports the GET method. Received {}.",
230+
method
231+
)));
232+
}
233+
234+
let file_fetch_handler = state.borrow_mut::<FH>();
235+
let (request, maybe_request_body, maybe_cancel_handle) =
236+
file_fetch_handler.fetch_file(url);
237+
let request_rid = state.resource_table.add(FetchRequestResource(request));
238+
let maybe_request_body_rid =
239+
maybe_request_body.map(|r| state.resource_table.add(r));
240+
let maybe_cancel_handle_rid = maybe_cancel_handle
241+
.map(|ch| state.resource_table.add(FetchCancelHandle(ch)));
242+
243+
(request_rid, maybe_request_body_rid, maybe_cancel_handle_rid)
244+
}
170245
"http" | "https" => {
171246
let permissions = state.borrow_mut::<FP>();
172247
permissions.check_net_url(&url)?;
@@ -400,7 +475,7 @@ impl Resource for FetchCancelHandle {
400475
}
401476
}
402477

403-
struct FetchRequestBodyResource {
478+
pub struct FetchRequestBodyResource {
404479
body: AsyncRefCell<mpsc::Sender<std::io::Result<Vec<u8>>>>,
405480
cancel: CancelHandle,
406481
}

runtime/build.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,14 @@ mod not_docs {
121121
deno_url::init(),
122122
deno_tls::init(),
123123
deno_web::init(deno_web::BlobStore::default(), Default::default()),
124-
deno_fetch::init::<Permissions>(
124+
deno_fetch::init::<Permissions, deno_fetch::DefaultFileFetchHandler>(
125125
"".to_owned(),
126126
None,
127127
None,
128128
None,
129129
None,
130130
None,
131+
deno_fetch::DefaultFileFetchHandler, // No enable_file_fetch
131132
),
132133
deno_websocket::init::<Permissions>("".to_owned(), None, None),
133134
deno_webstorage::init(None),

runtime/web_worker.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,13 +317,14 @@ impl WebWorker {
317317
deno_console::init(),
318318
deno_url::init(),
319319
deno_web::init(options.blob_store.clone(), Some(main_module.clone())),
320-
deno_fetch::init::<Permissions>(
320+
deno_fetch::init::<Permissions, deno_fetch::FsFetchHandler>(
321321
options.user_agent.clone(),
322322
options.root_cert_store.clone(),
323323
None,
324324
None,
325325
options.unsafely_ignore_certificate_errors.clone(),
326326
None,
327+
deno_fetch::FsFetchHandler,
327328
),
328329
deno_websocket::init::<Permissions>(
329330
options.user_agent.clone(),

runtime/worker.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,13 +101,14 @@ impl MainWorker {
101101
options.blob_store.clone(),
102102
options.bootstrap.location.clone(),
103103
),
104-
deno_fetch::init::<Permissions>(
104+
deno_fetch::init::<Permissions, deno_fetch::FsFetchHandler>(
105105
options.user_agent.clone(),
106106
options.root_cert_store.clone(),
107107
None,
108108
None,
109109
options.unsafely_ignore_certificate_errors.clone(),
110110
None,
111+
deno_fetch::FsFetchHandler,
111112
),
112113
deno_websocket::init::<Permissions>(
113114
options.user_agent.clone(),

0 commit comments

Comments
 (0)