1use std::any::Any;
8use std::collections::VecDeque;
9use std::future::{self, Future};
10use std::io::{self, Read, Seek, SeekFrom, Write};
11use std::mem;
12#[cfg(unix)]
13use std::os::unix::{
14 ffi::OsStrExt,
15 fs::{DirBuilderExt, MetadataExt, OpenOptionsExt, PermissionsExt},
16};
17#[cfg(target_os = "windows")]
18use std::os::windows::prelude::*;
19use std::path::{Path, PathBuf};
20use std::pin::Pin;
21use std::pin::pin;
22use std::sync::Arc;
23use std::task::{Context, Poll};
24use std::time::{Duration, SystemTime, UNIX_EPOCH};
25
26use bytes::{Buf, Bytes, BytesMut};
27use futures_util::{FutureExt, Stream, future::BoxFuture};
28use tokio::task;
29
30use libc;
31use reflink_copy::reflink_or_copy;
32
33use crate::davpath::DavPath;
34use crate::fs::*;
35use crate::localfs_macos::DUCacheBuilder;
36
37#[cfg(feature = "localfs")]
42#[inline]
43async fn blocking<F, R>(func: F) -> R
44where
45 F: FnOnce() -> R,
46 F: Send + 'static,
47 R: Send + 'static,
48{
49 match tokio::runtime::Handle::current().runtime_flavor() {
50 tokio::runtime::RuntimeFlavor::MultiThread => task::block_in_place(func),
51 _ => task::spawn_blocking(func).await.unwrap(),
52 }
53}
54
55#[derive(Debug, Clone)]
56struct LocalFsMetaData(std::fs::Metadata);
57
58#[derive(Clone)]
60pub struct LocalFs {
61 pub(crate) inner: Arc<LocalFsInner>,
62}
63
64pub(crate) struct LocalFsInner {
66 pub basedir: PathBuf,
67 #[allow(dead_code)]
68 pub public: bool,
69 pub case_insensitive: bool,
70 pub macos: bool,
71 pub is_file: bool,
72 pub fs_access_guard: Option<Box<dyn Fn() -> Box<dyn Any> + Send + Sync + 'static>>,
73}
74
75#[derive(Debug)]
76struct LocalFsFile {
77 file: Option<std::fs::File>,
78 buf: BytesMut,
79}
80
81struct LocalFsReadDir {
82 fs: LocalFs,
83 do_meta: ReadDirMeta,
84 buffer: VecDeque<io::Result<LocalFsDirEntry>>,
85 dir_cache: Option<DUCacheBuilder>,
86 iterator: Option<std::fs::ReadDir>,
87 fut: Option<BoxFuture<'static, ReadDirBatch>>,
88}
89
90enum Meta {
93 Data(io::Result<std::fs::Metadata>),
94 Fs(LocalFs),
95}
96
97#[allow(unused)]
99fn helper_create_directory(basedir: &Path, _new_dir_name: &str) {
100 let new_path = basedir.join(_new_dir_name);
101 std::fs::create_dir_all(&new_path)
102 .expect("Failed to create default CalDAV directory; verify that 'basedir' is correct.");
103}
104
105struct LocalFsDirEntry {
107 meta: Meta,
108 entry: std::fs::DirEntry,
109}
110
111impl LocalFs {
112 pub fn new<P: AsRef<Path>>(
121 base: P,
122 public: bool,
123 case_insensitive: bool,
124 macos: bool,
125 ) -> Box<LocalFs> {
126 let basedir = base.as_ref().to_path_buf();
127
128 #[cfg(feature = "caldav")]
129 helper_create_directory(&basedir, crate::caldav::DEFAULT_CALDAV_NAME);
130
131 #[cfg(feature = "carddav")]
132 helper_create_directory(&basedir, crate::carddav::DEFAULT_CARDDAV_NAME);
133
134 let inner = LocalFsInner {
135 basedir,
136 public,
137 macos,
138 case_insensitive,
139 is_file: false,
140 fs_access_guard: None,
141 };
142 Box::new({
143 LocalFs {
144 inner: Arc::new(inner),
145 }
146 })
147 }
148
149 pub fn new_file<P: AsRef<Path>>(file: P, public: bool) -> Box<LocalFs> {
154 let inner = LocalFsInner {
155 basedir: file.as_ref().to_path_buf(),
156 public,
157 macos: false,
158 case_insensitive: false,
159 is_file: true,
160 fs_access_guard: None,
161 };
162 Box::new({
163 LocalFs {
164 inner: Arc::new(inner),
165 }
166 })
167 }
168
169 #[doc(hidden)]
171 pub fn new_with_fs_access_guard<P: AsRef<Path>>(
172 base: P,
173 public: bool,
174 case_insensitive: bool,
175 macos: bool,
176 fs_access_guard: Option<Box<dyn Fn() -> Box<dyn Any> + Send + Sync + 'static>>,
177 ) -> Box<LocalFs> {
178 let basedir = base.as_ref().to_path_buf();
179
180 #[cfg(feature = "caldav")]
181 helper_create_directory(&basedir, crate::caldav::DEFAULT_CALDAV_NAME);
182
183 #[cfg(feature = "carddav")]
184 helper_create_directory(&basedir, crate::carddav::DEFAULT_CARDDAV_NAME);
185
186 let inner = LocalFsInner {
187 basedir,
188 public,
189 macos,
190 case_insensitive,
191 is_file: false,
192 fs_access_guard,
193 };
194 Box::new({
195 LocalFs {
196 inner: Arc::new(inner),
197 }
198 })
199 }
200
201 fn fspath_dbg(&self, path: &DavPath) -> PathBuf {
202 let mut pathbuf = self.inner.basedir.clone();
203 if !self.inner.is_file {
204 pathbuf.push(path.as_rel_ospath());
205 }
206 pathbuf
207 }
208
209 fn fspath(&self, path: &DavPath) -> PathBuf {
210 if self.inner.case_insensitive {
211 crate::localfs_windows::resolve(&self.inner.basedir, path)
212 } else {
213 let mut pathbuf = self.inner.basedir.clone();
214 if !self.inner.is_file {
215 pathbuf.push(path.as_rel_ospath());
216 }
217 pathbuf
218 }
219 }
220
221 #[doc(hidden)]
223 pub async fn blocking<F, R>(&self, func: F) -> R
224 where
225 F: FnOnce() -> R + Send + 'static,
226 R: Send + 'static,
227 {
228 let this = self.clone();
229 blocking(move || {
230 let _guard = this.inner.fs_access_guard.as_ref().map(|f| f());
231 func()
232 })
233 .await
234 }
235}
236
237impl DavFileSystem for LocalFs {
240 fn metadata<'a>(&'a self, davpath: &'a DavPath) -> FsFuture<'a, Box<dyn DavMetaData>> {
241 async move {
242 if let Some(meta) = self.is_virtual(davpath) {
243 return Ok(meta);
244 }
245 let path = self.fspath(davpath);
246 if self.is_notfound(&path) {
247 return Err(FsError::NotFound);
248 }
249 self.blocking(move || match std::fs::metadata(path) {
250 Ok(meta) => Ok(Box::new(LocalFsMetaData(meta)) as Box<dyn DavMetaData>),
251 Err(e) => Err(e.into()),
252 })
253 .await
254 }
255 .boxed()
256 }
257
258 fn symlink_metadata<'a>(&'a self, davpath: &'a DavPath) -> FsFuture<'a, Box<dyn DavMetaData>> {
259 async move {
260 if let Some(meta) = self.is_virtual(davpath) {
261 return Ok(meta);
262 }
263 let path = self.fspath(davpath);
264 if self.is_notfound(&path) {
265 return Err(FsError::NotFound);
266 }
267 self.blocking(move || match std::fs::symlink_metadata(path) {
268 Ok(meta) => Ok(Box::new(LocalFsMetaData(meta)) as Box<dyn DavMetaData>),
269 Err(e) => Err(e.into()),
270 })
271 .await
272 }
273 .boxed()
274 }
275
276 fn read_dir<'a>(
279 &'a self,
280 davpath: &'a DavPath,
281 meta: ReadDirMeta,
282 ) -> FsFuture<'a, FsStream<Box<dyn DavDirEntry>>> {
283 async move {
284 trace!("FS: read_dir {:?}", self.fspath_dbg(davpath));
285 let path = self.fspath(davpath);
286 let path2 = path.clone();
287 let iter = self.blocking(move || std::fs::read_dir(&path)).await;
288 match iter {
289 Ok(iterator) => {
290 let strm = LocalFsReadDir {
291 fs: self.clone(),
292 do_meta: meta,
293 buffer: VecDeque::new(),
294 dir_cache: self.dir_cache_builder(path2),
295 iterator: Some(iterator),
296 fut: None,
297 };
298 Ok(Box::pin(strm) as FsStream<Box<dyn DavDirEntry>>)
299 }
300 Err(e) => Err(e.into()),
301 }
302 }
303 .boxed()
304 }
305
306 fn open<'a>(
307 &'a self,
308 path: &'a DavPath,
309 options: OpenOptions,
310 ) -> FsFuture<'a, Box<dyn DavFile>> {
311 async move {
312 trace!("FS: open {:?}", self.fspath_dbg(path));
313 if self.is_forbidden(path) {
314 return Err(FsError::Forbidden);
315 }
316 #[cfg(unix)]
317 let mode = if self.inner.public { 0o644 } else { 0o600 };
318 let path = self.fspath(path);
319 self.blocking(move || {
320 #[cfg(unix)]
321 let res = std::fs::OpenOptions::new()
322 .read(options.read)
323 .write(options.write)
324 .append(options.append)
325 .truncate(options.truncate)
326 .create(options.create)
327 .create_new(options.create_new)
328 .mode(mode)
329 .open(path);
330 #[cfg(windows)]
331 let res = std::fs::OpenOptions::new()
332 .read(options.read)
333 .write(options.write)
334 .append(options.append)
335 .truncate(options.truncate)
336 .create(options.create)
337 .create_new(options.create_new)
338 .open(path);
339 match res {
340 Ok(file) => Ok(Box::new(LocalFsFile {
341 file: Some(file),
342 buf: BytesMut::new(),
343 }) as Box<dyn DavFile>),
344 Err(e) => Err(e.into()),
345 }
346 })
347 .await
348 }
349 .boxed()
350 }
351
352 fn create_dir<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()> {
353 async move {
354 trace!("FS: create_dir {:?}", self.fspath_dbg(path));
355 if self.is_forbidden(path) {
356 return Err(FsError::Forbidden);
357 }
358 #[cfg(unix)]
359 let mode = if self.inner.public { 0o755 } else { 0o700 };
360 let path = self.fspath(path);
361 self.blocking(move || {
362 #[cfg(unix)]
363 {
364 std::fs::DirBuilder::new()
365 .mode(mode)
366 .create(path)
367 .map_err(|e| e.into())
368 }
369 #[cfg(windows)]
370 {
371 std::fs::DirBuilder::new()
372 .create(path)
373 .map_err(|e| e.into())
374 }
375 })
376 .await
377 }
378 .boxed()
379 }
380
381 fn remove_dir<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()> {
382 async move {
383 trace!("FS: remove_dir {:?}", self.fspath_dbg(path));
384 let path = self.fspath(path);
385 self.blocking(move || std::fs::remove_dir(path).map_err(|e| e.into()))
386 .await
387 }
388 .boxed()
389 }
390
391 fn remove_file<'a>(&'a self, path: &'a DavPath) -> FsFuture<'a, ()> {
392 async move {
393 trace!("FS: remove_file {:?}", self.fspath_dbg(path));
394 if self.is_forbidden(path) {
395 return Err(FsError::Forbidden);
396 }
397 let path = self.fspath(path);
398 self.blocking(move || std::fs::remove_file(path).map_err(|e| e.into()))
399 .await
400 }
401 .boxed()
402 }
403
404 fn rename<'a>(&'a self, from: &'a DavPath, to: &'a DavPath) -> FsFuture<'a, ()> {
405 async move {
406 trace!(
407 "FS: rename {:?} {:?}",
408 self.fspath_dbg(from),
409 self.fspath_dbg(to)
410 );
411 if self.is_forbidden(from) || self.is_forbidden(to) {
412 return Err(FsError::Forbidden);
413 }
414 let frompath = self.fspath(from);
415 let topath = self.fspath(to);
416 self.blocking(move || {
417 match std::fs::rename(&frompath, &topath) {
418 Ok(v) => Ok(v),
419 Err(e) => {
420 if e.raw_os_error() == Some(libc::ENOTDIR) && frompath.is_dir() {
424 let _ = std::fs::remove_file(&topath);
426 std::fs::rename(frompath, topath).map_err(|e| e.into())
427 } else {
428 Err(e.into())
429 }
430 }
431 }
432 })
433 .await
434 }
435 .boxed()
436 }
437
438 fn copy<'a>(&'a self, from: &'a DavPath, to: &'a DavPath) -> FsFuture<'a, ()> {
439 async move {
440 trace!(
441 "FS: copy {:?} {:?}",
442 self.fspath_dbg(from),
443 self.fspath_dbg(to)
444 );
445 if self.is_forbidden(from) || self.is_forbidden(to) {
446 return Err(FsError::Forbidden);
447 }
448 let path_from = self.fspath(from);
449 let path_to = self.fspath(to);
450
451 match self
452 .blocking(move || reflink_or_copy(path_from, path_to))
453 .await
454 {
455 Ok(_) => Ok(()),
456 Err(e) => {
457 debug!(
458 "copy({:?}, {:?}) failed: {}",
459 self.fspath_dbg(from),
460 self.fspath_dbg(to),
461 e
462 );
463 Err(e.into())
464 }
465 }
466 }
467 .boxed()
468 }
469}
470
471struct ReadDirBatch {
473 iterator: Option<std::fs::ReadDir>,
474 buffer: VecDeque<io::Result<LocalFsDirEntry>>,
475}
476
477fn read_batch(
480 iterator: Option<std::fs::ReadDir>,
481 fs: LocalFs,
482 do_meta: ReadDirMeta,
483) -> ReadDirBatch {
484 let mut buffer = VecDeque::new();
485 let mut iterator = match iterator {
486 Some(i) => i,
487 None => {
488 return ReadDirBatch {
489 buffer,
490 iterator: None,
491 };
492 }
493 };
494 let _guard = match do_meta {
495 ReadDirMeta::None => None,
496 _ => fs.inner.fs_access_guard.as_ref().map(|f| f()),
497 };
498 for _ in 0..256 {
499 match iterator.next() {
500 Some(Ok(entry)) => {
501 let meta = match do_meta {
502 ReadDirMeta::Data => Meta::Data(std::fs::metadata(entry.path())),
503 ReadDirMeta::DataSymlink => Meta::Data(entry.metadata()),
504 ReadDirMeta::None => Meta::Fs(fs.clone()),
505 };
506 let d = LocalFsDirEntry { meta, entry };
507 buffer.push_back(Ok(d))
508 }
509 Some(Err(e)) => {
510 buffer.push_back(Err(e));
511 break;
512 }
513 None => break,
514 }
515 }
516 ReadDirBatch {
517 buffer,
518 iterator: Some(iterator),
519 }
520}
521
522impl LocalFsReadDir {
523 fn read_batch(&mut self) -> BoxFuture<'static, ReadDirBatch> {
528 let iterator = self.iterator.take();
529 let fs = self.fs.clone();
530 let do_meta = self.do_meta;
531
532 let fut: BoxFuture<ReadDirBatch> =
533 blocking(move || read_batch(iterator, fs, do_meta)).boxed();
534 fut
535 }
536}
537
538impl Stream for LocalFsReadDir {
540 type Item = FsResult<Box<dyn DavDirEntry>>;
541
542 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
543 let this = Pin::into_inner(self);
544
545 if this.buffer.is_empty() {
547 if this.fut.is_none() {
549 if this.iterator.is_none() {
550 return Poll::Ready(None);
551 }
552 this.fut = Some(this.read_batch());
553 }
554
555 let mut fut = pin!(this.fut.as_mut().unwrap());
557 match Pin::new(&mut fut).poll(cx) {
558 Poll::Ready(batch) => {
559 this.fut.take();
560 if let Some(ref mut nb) = this.dir_cache {
561 batch.buffer.iter().for_each(|e| {
562 if let Ok(e) = e {
563 nb.add(e.entry.file_name());
564 }
565 });
566 }
567 this.buffer = batch.buffer;
568 this.iterator = batch.iterator;
569 }
570 Poll::Pending => return Poll::Pending,
571 }
572 }
573
574 match this.buffer.pop_front() {
576 Some(Ok(item)) => Poll::Ready(Some(Ok(Box::new(item)))),
577 Some(Err(err)) => {
578 this.iterator.take();
580 if let Some(ref mut nb) = this.dir_cache {
582 nb.finish();
583 }
584 Poll::Ready(Some(Err(err.into())))
586 }
587 None => {
588 this.iterator.take();
590 if let Some(ref mut nb) = this.dir_cache {
592 nb.finish();
593 }
594 Poll::Ready(None)
596 }
597 }
598 }
599}
600
601enum Is {
602 File,
603 Dir,
604 Symlink,
605}
606
607impl LocalFsDirEntry {
608 async fn is_a(&self, is: Is) -> FsResult<bool> {
609 match self.meta {
610 Meta::Data(Ok(ref meta)) => Ok(match is {
611 Is::File => meta.file_type().is_file(),
612 Is::Dir => meta.file_type().is_dir(),
613 Is::Symlink => meta.file_type().is_symlink(),
614 }),
615 Meta::Data(Err(ref e)) => Err(e.into()),
616 Meta::Fs(ref fs) => {
617 let fullpath = self.entry.path();
618 let ft = fs
619 .blocking(move || std::fs::metadata(&fullpath))
620 .await?
621 .file_type();
622 Ok(match is {
623 Is::File => ft.is_file(),
624 Is::Dir => ft.is_dir(),
625 Is::Symlink => ft.is_symlink(),
626 })
627 }
628 }
629 }
630}
631
632impl DavDirEntry for LocalFsDirEntry {
633 fn metadata(&'_ self) -> FsFuture<'_, Box<dyn DavMetaData>> {
634 match self.meta {
635 Meta::Data(ref meta) => {
636 let m = match meta {
637 Ok(meta) => Ok(Box::new(LocalFsMetaData(meta.clone())) as Box<dyn DavMetaData>),
638 Err(e) => Err(e.into()),
639 };
640 Box::pin(future::ready(m))
641 }
642 Meta::Fs(ref fs) => {
643 let fullpath = self.entry.path();
644 fs.blocking(move || match std::fs::metadata(&fullpath) {
645 Ok(meta) => Ok(Box::new(LocalFsMetaData(meta)) as Box<dyn DavMetaData>),
646 Err(e) => Err(e.into()),
647 })
648 .boxed()
649 }
650 }
651 }
652
653 #[cfg(unix)]
654 fn name(&self) -> Vec<u8> {
655 self.entry.file_name().as_bytes().to_vec()
656 }
657
658 #[cfg(windows)]
659 fn name(&self) -> Vec<u8> {
660 self.entry.file_name().to_str().unwrap().as_bytes().to_vec()
661 }
662
663 fn is_dir(&'_ self) -> FsFuture<'_, bool> {
664 Box::pin(self.is_a(Is::Dir))
665 }
666
667 fn is_file(&'_ self) -> FsFuture<'_, bool> {
668 Box::pin(self.is_a(Is::File))
669 }
670
671 fn is_symlink(&'_ self) -> FsFuture<'_, bool> {
672 Box::pin(self.is_a(Is::Symlink))
673 }
674}
675
676impl DavFile for LocalFsFile {
677 fn metadata(&'_ mut self) -> FsFuture<'_, Box<dyn DavMetaData>> {
678 async move {
679 let file = self.file.take().unwrap();
680 let (meta, file) = blocking(move || (file.metadata(), file)).await;
681 self.file = Some(file);
682 Ok(Box::new(LocalFsMetaData(meta?)) as Box<dyn DavMetaData>)
683 }
684 .boxed()
685 }
686
687 fn write_bytes(&'_ mut self, buf: Bytes) -> FsFuture<'_, ()> {
688 async move {
689 let mut file = self.file.take().unwrap();
690 let (res, file) = blocking(move || (file.write_all(&buf), file)).await;
691 self.file = Some(file);
692 res.map_err(|e| e.into())
693 }
694 .boxed()
695 }
696
697 fn write_buf(&'_ mut self, mut buf: Box<dyn Buf + Send>) -> FsFuture<'_, ()> {
698 async move {
699 let mut file = self.file.take().unwrap();
700 let (res, file) = blocking(move || {
701 while buf.remaining() > 0 {
702 let n = match file.write(buf.chunk()) {
703 Ok(n) => n,
704 Err(e) => return (Err(e), file),
705 };
706 buf.advance(n);
707 }
708 (Ok(()), file)
709 })
710 .await;
711 self.file = Some(file);
712 res.map_err(|e| e.into())
713 }
714 .boxed()
715 }
716
717 fn read_bytes(&'_ mut self, count: usize) -> FsFuture<'_, Bytes> {
718 async move {
719 let mut file = self.file.take().unwrap();
720 let mut buf = mem::take(&mut self.buf);
721 let (res, file, buf) = blocking(move || {
722 buf.reserve(count);
723 let res = unsafe {
724 buf.set_len(count);
725 file.read(&mut buf).map(|n| {
726 buf.set_len(n);
727 buf.split().freeze()
728 })
729 };
730 (res, file, buf)
731 })
732 .await;
733 self.file = Some(file);
734 self.buf = buf;
735 res.map_err(|e| e.into())
736 }
737 .boxed()
738 }
739
740 fn seek(&'_ mut self, pos: SeekFrom) -> FsFuture<'_, u64> {
741 async move {
742 let mut file = self.file.take().unwrap();
743 let (res, file) = blocking(move || (file.seek(pos), file)).await;
744 self.file = Some(file);
745 res.map_err(|e| e.into())
746 }
747 .boxed()
748 }
749
750 fn flush(&'_ mut self) -> FsFuture<'_, ()> {
751 async move {
752 let mut file = self.file.take().unwrap();
753 let (res, file) = blocking(move || (file.flush(), file)).await;
754 self.file = Some(file);
755 res.map_err(|e| e.into())
756 }
757 .boxed()
758 }
759}
760
761impl DavMetaData for LocalFsMetaData {
762 fn len(&self) -> u64 {
763 self.0.len()
764 }
765 fn created(&self) -> FsResult<SystemTime> {
766 self.0.created().map_err(|e| e.into())
767 }
768 fn modified(&self) -> FsResult<SystemTime> {
769 self.0.modified().map_err(|e| e.into())
770 }
771 fn accessed(&self) -> FsResult<SystemTime> {
772 self.0.accessed().map_err(|e| e.into())
773 }
774
775 #[cfg(unix)]
776 fn status_changed(&self) -> FsResult<SystemTime> {
777 Ok(UNIX_EPOCH + Duration::new(self.0.ctime() as u64, 0))
778 }
779
780 #[cfg(windows)]
781 fn status_changed(&self) -> FsResult<SystemTime> {
782 Ok(UNIX_EPOCH + Duration::from_nanos(self.0.creation_time() - 116444736000000000))
783 }
784
785 fn is_dir(&self) -> bool {
786 self.0.is_dir()
787 }
788 fn is_file(&self) -> bool {
789 self.0.is_file()
790 }
791 fn is_symlink(&self) -> bool {
792 self.0.file_type().is_symlink()
793 }
794 #[cfg(feature = "caldav")]
795 fn is_calendar(&self, path: &DavPath) -> bool {
796 crate::caldav::is_path_in_caldav_directory(path)
797 }
798 #[cfg(feature = "carddav")]
799 fn is_addressbook(&self, path: &DavPath) -> bool {
800 crate::carddav::is_path_in_carddav_directory(path)
801 }
802
803 #[cfg(unix)]
804 fn executable(&self) -> FsResult<bool> {
805 if self.0.is_file() {
806 return Ok((self.0.permissions().mode() & 0o100) > 0);
807 }
808 Err(FsError::NotImplemented)
809 }
810
811 #[cfg(windows)]
812 fn executable(&self) -> FsResult<bool> {
813 Err(FsError::NotImplemented)
815 }
816
817 #[cfg(unix)]
819 fn etag(&self) -> Option<String> {
820 let modified = self.0.modified().ok()?;
821 let t = modified.duration_since(UNIX_EPOCH).ok()?;
822 let t = t.as_secs() * 1000000 + t.subsec_nanos() as u64 / 1000;
823 if self.is_file() {
824 Some(format!("{:x}-{:x}-{:x}", self.0.ino(), self.0.len(), t))
825 } else {
826 Some(format!("{:x}-{:x}", self.0.ino(), t))
827 }
828 }
829
830 #[cfg(windows)]
832 fn etag(&self) -> Option<String> {
833 let modified = self.0.modified().ok()?;
834 let t = modified.duration_since(UNIX_EPOCH).ok()?;
835 let t = t.as_secs() * 1000000 + t.subsec_nanos() as u64 / 1000;
836 if self.is_file() {
837 Some(format!("{:x}-{:x}", self.0.len(), t))
838 } else {
839 Some(format!("{:x}", t))
840 }
841 }
842}