Skip to content

Commit

Permalink
$mol_file webdav
Browse files Browse the repository at this point in the history
  • Loading branch information
zerkalica committed Nov 12, 2024
1 parent af18ed6 commit 3be1c67
Show file tree
Hide file tree
Showing 7 changed files with 331 additions and 42 deletions.
34 changes: 34 additions & 0 deletions file/demo/demo.view.tree
Original file line number Diff line number Diff line change
@@ -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
26 changes: 26 additions & 0 deletions file/demo/demo.view.ts
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
17 changes: 17 additions & 0 deletions file/demo/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!-- Disable quirks mode -->
<!doctype html>

<!-- Allow height:100% in body -->
<html style=" height: 100% ">

<!-- Force utf-8 encoding -->
<meta charset="utf-8" />

<!-- Disable mobile browser auto zoom, $mol is adaptive -->
<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1">

<!-- autobind component to element on load -->
<body mol_view_root="$mol_file_demo">

<!-- link to autogenerated test js bundle -->
<script src="web.js"></script>
47 changes: 22 additions & 25 deletions file/file.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 )
}
Expand Down Expand Up @@ -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<Uint8Array>
}

@ $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<Uint8Array>
}

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<Uint8Array>
}

open( ... modes: readonly $mol_file_mode[] ) {
Expand All @@ -216,5 +212,6 @@ namespace $ {

}


$.$mol_file = $mol_file_node
}
27 changes: 18 additions & 9 deletions file/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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() ) {
/*
Если родитель удалился, надо ресетнуть все дочерние на любой глубине
Родитель может удалиться, потом создасться, а дочерняя папка только удалиться.
Expand Down Expand Up @@ -167,7 +164,7 @@ namespace $ {

static watch_off<Result>(side_effect: () => Result, affected_dir: string) {
// ждем, пока выполнится предыдущий watch_off
const unlock = this.lock.grab()
const unlock = () => {} // this.lock.grab()
this.watching_off(affected_dir)

try {
Expand Down Expand Up @@ -205,9 +202,19 @@ namespace $ {
protected kids() {
return [] as readonly this[]
}
static headers() { return {} as Record<string, string> }
headers() { return (this.constructor as typeof $mol_file).headers() }

@ $mol_mem_key
readable(opts: { start?: number, end?: number }) {
return new ReadableStream<Uint8Array>
}

@ $mol_mem_key
writable(opts: { start?: number }) {
return new WritableStream<Uint8Array>
}

readable(opts: { modes: readonly $mol_file_mode[] }) { return new ReadableStream<Uint8Array>() }
writable(opts: { modes: readonly $mol_file_mode[] }) { return new WritableStream<Uint8Array>() }
// open( ... modes: readonly $mol_file_mode[] ) { return 0 }

@ $mol_mem
Expand Down Expand Up @@ -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(
Expand Down
129 changes: 121 additions & 8 deletions file/file.web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`)
Expand All @@ -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
Expand Down
Loading

0 comments on commit 3be1c67

Please sign in to comment.