diff --git a/file/demo/demo.view.tree b/file/demo/demo.view.tree
new file mode 100644
index 0000000000..92521cc3aa
--- /dev/null
+++ b/file/demo/demo.view.tree
@@ -0,0 +1,34 @@
+$mol_file_demo $mol_example
+ title \Webdav demo
+ actual_url? \
+ sub /
+ <= Left $mol_page
+ title \Webdav controls
+ body /
+ <= Form $mol_form
+ body /
+ <= Webdav_url_field $mol_form_field
+ name \WebDav url
+ Content <= Webdav_url_control $mol_string
+ hint \http://
+ value? <=> webdav_url? \
+ submit? <=> refresh? null
+ buttons /
+ <= Update $mol_button_major
+ title \Update
+ click? <=> refresh? null
+ <= Right $mol_page
+ title \Files
+ body / <= Files $mol_list
+ minimal_width 320
+ rows <= file_list /$mol_view
+ <= File*0 $mol_card
+ title <= file_name* \
+ status <= file_type* \file
+ tags /
+ \$mol_button
+ \form
+ \file upload
+ \webdav
+ aspects /
+ \Widget/Form
diff --git a/file/demo/demo.view.ts b/file/demo/demo.view.ts
new file mode 100644
index 0000000000..07a1589190
--- /dev/null
+++ b/file/demo/demo.view.ts
@@ -0,0 +1,26 @@
+namespace $.$$ {
+ export class $mol_file_demo extends $.$mol_file_demo {
+ @ $mol_mem
+ root() {
+ if (! this.actual_url().match(/^https?:\/\/.+/)) return null
+ return this.$.$mol_file.absolute(this.actual_url())
+ }
+
+ @ $mol_mem
+ override file_list() {
+ return this.root()?.sub().map(file => this.File(file.path())) ?? []
+ }
+
+ @ $mol_mem
+ override refresh(e?: Event) {
+ this.actual_url(this.webdav_url())
+ }
+
+ file_name(path: string) {
+ return this.$.$mol_file.absolute(path).relate()
+ }
+ file_type(path: string) {
+ return this.$.$mol_file.absolute(path).type()
+ }
+ }
+}
diff --git a/file/demo/index.html b/file/demo/index.html
new file mode 100644
index 0000000000..b2b9e76818
--- /dev/null
+++ b/file/demo/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/file/file.node.ts b/file/file.node.ts
index 8933cf4c99..b949935e84 100644
--- a/file/file.node.ts
+++ b/file/file.node.ts
@@ -38,6 +38,8 @@ namespace $ {
append = $node.fs.constants.O_APPEND,
}
+ export type $mol_file_mode = keyof typeof file_modes
+
function mode_mask(modes: readonly $mol_file_mode[]) {
return modes.reduce( ( res, mode )=> res | file_modes[ mode ], 0 )
}
@@ -175,36 +177,30 @@ namespace $ {
}
@ $mol_mem_key
- override readable({ modes }: { modes: readonly $mol_file_mode[] }) {
- return this.handle(modes).readableWebStream({ type: 'bytes' }) as ReadableStream
- }
-
- @ $mol_mem_key
- override writable({ modes, start }: { modes: readonly $mol_file_mode[], start?: number }) {
- const { Writable } = $node['node:stream'] as typeof import('stream')
- const stream = this.handle(modes).createWriteStream({
- start,
- // encoding?: BufferEncoding | null | undefined;
- // autoClose?: boolean | undefined;
- // emitClose?: boolean | undefined;
- // start?: number | undefined;
- // highWaterMark?: number | undefined;
- // flush?: boolean | undefined;
+ override readable(opts: { start?: number, end?: number }) {
+ const { Readable } = $node['node:stream'] as typeof import('stream')
+ const stream = $node.fs.createReadStream(this.path(), {
+ flags: 'r',
+ autoClose: true,
+ start: opts?.start,
+ end: opts?.end,
+ encoding: 'binary',
})
- const web = Writable.toWeb(stream)
-
- return Object.assign(web, {
- destructor: () => web.close()
- })
+ return Readable.toWeb(stream) as ReadableStream
}
- protected handle(modes: readonly $mol_file_mode[]) {
- const mode = mode_mask(modes)
-
- const handle = $mol_wire_sync($node.fs.promises).open(this.path(), mode)
+ @ $mol_mem
+ override writable(opts?: { start?: number }) {
+ const { Writable } = $node['node:stream'] as typeof import('stream')
+ const stream = $node.fs.createWriteStream(this.path(), {
+ flags: 'w+',
+ autoClose: true,
+ start: opts?.start,
+ encoding: 'binary',
+ })
- return $mol_wire_sync(handle)
+ return Writable.toWeb(stream) as WritableStream
}
open( ... modes: readonly $mol_file_mode[] ) {
@@ -216,5 +212,6 @@ namespace $ {
}
+
$.$mol_file = $mol_file_node
}
diff --git a/file/file.ts b/file/file.ts
index ff0a4cff43..1a15eb5935 100644
--- a/file/file.ts
+++ b/file/file.ts
@@ -10,8 +10,6 @@ namespace $ {
ctime: Date
}
- export type $mol_file_mode = 'create' | 'exists_truncate' | 'exists_fail' | 'read_only' | 'write_only' | 'read_write' | 'append'
-
// export class $mol_file_not_found extends Error {}
export class $mol_file extends $mol_object {
@@ -46,8 +44,7 @@ namespace $ {
// Отслеживать проверку наличия родительской папки не стоит до корня диска
// Лучше ограничить mam-ом
- const root = this.$.$mol_file.watch_root ?? this
- if ( path !== root ) {
+ if ( path !== this.$.$mol_file.watch_root && path !== parent.path() ) {
/*
Если родитель удалился, надо ресетнуть все дочерние на любой глубине
Родитель может удалиться, потом создасться, а дочерняя папка только удалиться.
@@ -167,7 +164,7 @@ namespace $ {
static watch_off(side_effect: () => Result, affected_dir: string) {
// ждем, пока выполнится предыдущий watch_off
- const unlock = this.lock.grab()
+ const unlock = () => {} // this.lock.grab()
this.watching_off(affected_dir)
try {
@@ -205,9 +202,19 @@ namespace $ {
protected kids() {
return [] as readonly this[]
}
+ static headers() { return {} as Record }
+ headers() { return (this.constructor as typeof $mol_file).headers() }
+
+ @ $mol_mem_key
+ readable(opts: { start?: number, end?: number }) {
+ return new ReadableStream
+ }
+
+ @ $mol_mem_key
+ writable(opts: { start?: number }) {
+ return new WritableStream
+ }
- readable(opts: { modes: readonly $mol_file_mode[] }) { return new ReadableStream() }
- writable(opts: { modes: readonly $mol_file_mode[] }) { return new WritableStream() }
// open( ... modes: readonly $mol_file_mode[] ) { return 0 }
@ $mol_mem
@@ -359,8 +366,10 @@ namespace $ {
throw new Error('implement')
}
- relate( base?: $mol_file ): string {
- throw new Error('implement')
+ relate( base = ( this.constructor as typeof $mol_file ).relative( '.' )): string {
+ const base_path = base.path()
+ const path = this.path()
+ return path.startsWith(base_path) ? path.slice(base_path.length) : path
}
find(
diff --git a/file/file.web.ts b/file/file.web.ts
index 798909b5b0..f70d4942af 100644
--- a/file/file.web.ts
+++ b/file/file.web.ts
@@ -10,6 +10,15 @@ namespace $ {
? new URL( '.' , ($mol_dom_context.document.currentScript as any)['src'] ).toString()
: ''
+ protected override read() {
+ const response = this.$.$mol_fetch.response(this.path(), {
+ headers: this.headers()
+ })
+ if (response.native.status === 404) return new Uint8Array
+
+ return new Uint8Array(response.buffer())
+ }
+
@ $mol_mem
override buffer( next? : Uint8Array ) {
if (next !== undefined) throw new Error(`Saving content not supported: ${this.path}`)
@@ -21,34 +30,138 @@ namespace $ {
return new Uint8Array(response.buffer())
}
+ protected fetch(init: RequestInit) {
+ return this.$.$mol_fetch.success(this.path(), {
+ ...init,
+ headers: {
+ ...this.headers(),
+ ...init.headers,
+ }
+ })
+ }
+
+ protected override write( body : Uint8Array ) {
+ this.fetch({ method: 'PUT', body })
+ }
+
+ @ $mol_mem_key
+ override readable( opts: { start?: number; end?: number } ) {
+ return this.fetch({
+ headers: ! opts.start ? {} : {
+ 'Range': `bytes=${opts.start}-${opts.end ?? ''}`
+ }
+ }).stream() || $mol_fail(new Error('Not found'))
+ }
+
override resolve( path : string ) {
let res = this.path() + '/' + path
while( true ) {
let prev = res
+ // foo/../ -> /
res = res.replace( /\/[^\/.]+\/\.\.\// , '/' )
if( prev === res ) break
}
-
+
+ // http://localhost/.. -> http://localhost
+ res = res.replace(/\/\.\.\/?$/, '')
+
+ if (res === this.path()) return this
+
return ( this.constructor as typeof $mol_file ).absolute( res ) as this
}
protected override ensure() {
- throw new Error('$mol_file_web.ensure() not implemented')
- }
+ this.fetch({ method: 'MKCOL' })
+ }
protected override drop() {
- throw new Error('$mol_file_web.drop() not implemented')
+ this.fetch({ method: 'DELETE' })
+ }
+
+ protected override copy(to: string) {
+ this.fetch({
+ method: 'COPY',
+ headers: { Destination: to }
+ })
}
protected override kids() : readonly this[] {
- throw new Error('$mol_file_web.sub() not implemented')
+ const response = this.fetch({ method: 'PROPFIND' })
+ const xml = response.xml()
+ const info = webdav_list(xml)
+
+ const kids = info.map(({ path, stat }) => {
+ const file = this.resolve(path)
+ file.stat(stat, 'virt')
+ return file
+ })
+
+ return kids
}
-
- override relate( base = ( this.constructor as typeof $mol_file ).relative( '.' )): string {
- throw new Error('$mol_file_web.relate() not implemented')
+
+ protected override info() {
+ return this.stat_make(0)
+ // return this.kids()?.at(0)?.stat() ?? null
+ }
+
+ }
+
+ function webdav_list(xml: Document) {
+ const result = []
+
+ for (const multistatus of xml.childNodes) {
+ if (multistatus.nodeName !== 'D:multistatus') continue
+
+ for (const response of multistatus.childNodes) {
+ let path
+
+ if (response.nodeName === 'D:href') path = response.textContent ?? ''
+
+ if (! path ) continue
+ if ( response.nodeName !== 'D:propstat') continue
+
+ const stat = webdav_stat(response)
+
+ result.push({ path, stat })
+ }
+ }
+
+ return result
+ }
+
+ function webdav_stat(prop_stat: ChildNode) {
+ const now = new Date()
+ const stat: $mol_file_stat = {
+ type: 'file',
+ size: 0,
+ atime: now,
+ mtime: now,
+ ctime: now,
+ }
+
+ for (const prop of prop_stat.childNodes) {
+ if (prop.nodeName !== 'D:prop') continue
+
+ for (const value of prop.childNodes) {
+ const name = value.nodeName
+ const text = value.textContent ?? ''
+
+ if (name === 'D:getcontenttype') {
+ stat.type = text.endsWith('directory') ? 'dir' : 'file'
+ }
+
+ if (name === 'D:getcontentlength') {
+ stat.size = Number(value.textContent || '0')
+ if (Number.isNaN(stat.size)) stat.size = 0
+ }
+
+ if (name === 'D:getlastmodified') stat.mtime = stat.atime = new Date(text)
+ if (name === 'D:creationdate') stat.ctime = new Date(text)
+ }
}
+ return stat
}
$.$mol_file = $mol_file_web
diff --git a/file/progress/progress.ts b/file/progress/progress.ts
new file mode 100644
index 0000000000..5843c59842
--- /dev/null
+++ b/file/progress/progress.ts
@@ -0,0 +1,93 @@
+namespace $ {
+ export class $mol_file_progress extends $mol_object {
+ @ $mol_mem
+ processed(next?: number): number {
+ return ( $mol_wire_probe(( ) => this.processed()) ?? 0) + (next ?? 0)
+ }
+
+ protected attach() { return {
+ destructor() {}
+ } }
+
+ @ $mol_mem
+ done(next?: boolean) {
+ if (next === undefined) this.attach()
+ return next ?? false
+ }
+ }
+
+ export class $mol_file_progress_write extends $mol_file_progress {
+ constructor(
+ protected readable: ReadableStream,
+ readonly native: WritableStream,
+ ) {
+ super()
+ }
+
+ @ $mol_mem
+ protected override attach() {
+ const writer = this.native.getWriter()
+ const write = writer.write.bind(writer)
+
+ writer.write = async (chunk: Uint8Array) => {
+ try {
+ await write(chunk)
+ } catch (error) {
+ this.done(error as boolean)
+ $mol_fail_hidden(error)
+ }
+ this.processed(chunk.length)
+ }
+
+ this.readable.pipeTo(this.native)
+ this.readable_progress = new $mol_file_progress_read(this.readable)
+
+ return super.attach()
+ }
+
+ readable_progress = null as null | $mol_file_progress_read
+
+ @ $mol_mem
+ done(next?: boolean) {
+ if (next === undefined) {
+ this.attach()
+ const progress = this.readable_progress!
+
+ return progress.done() && progress.processed() === this.processed()
+ }
+
+ return next
+ }
+
+ }
+
+ export class $mol_file_progress_read extends $mol_file_progress {
+ constructor(
+ readonly native: ReadableStream
+ ) {
+ super()
+ }
+
+ @ $mol_mem
+ protected override attach() {
+ const reader = this.native.getReader()
+ const read = reader.read.bind(reader)
+
+ reader.read = async () => {
+ try {
+ const result = await read()
+ this.processed(result.value?.length ?? 0)
+ if (result.done) this.done(true)
+ return result
+ } catch (error) {
+ this.done(error as boolean)
+ $mol_fail_hidden(error)
+ }
+ }
+
+ return super.attach()
+ }
+
+
+ }
+}