Skip to content

Commit

Permalink
feat(thumbnail): Polyfilling async thumbnail for being compatible
Browse files Browse the repository at this point in the history
Signed-off-by: Vincent Boutour <[email protected]>
  • Loading branch information
ViBiOh committed Jul 26, 2022
1 parent e3f3e73 commit 5ac65ec
Show file tree
Hide file tree
Showing 12 changed files with 170 additions and 148 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
# scripts
scripts/
/scripts/

# Golang
bin/
release/
coverage.*
.env

# NPM
node_modules/

# Fibr
.fibr/
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ test:
bench:
go test $(PACKAGES) -bench . -benchmem -run Benchmark.*

## build: Build the javascript polyfill.
.PHONY: build-js
build-js:
npm install --no-save --ignore-scripts @swc/cli @swc/core regenerator-runtime
npx spack

## build: Build the application.
.PHONY: build
build:
Expand All @@ -94,5 +100,5 @@ build:
.PHONY: run
run:
$(MAIN_RUNNER) \
-ignorePattern ".git" \
-ignorePattern ".git|node_modules" \
-authUsers "1:`htpasswd -nBb admin admin`"
2 changes: 1 addition & 1 deletion cmd/fibr/fibr.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func main() {
loggerConfig := logger.Flags(fs, "logger")
tracerConfig := tracer.Flags(fs, "tracer")
prometheusConfig := prometheus.Flags(fs, "prometheus", flags.NewOverride("Gzip", false))
owaspConfig := owasp.Flags(fs, "", flags.NewOverride("FrameOptions", "SAMEORIGIN"), flags.NewOverride("Csp", "default-src 'self'; base-uri 'self'; script-src 'httputils-nonce' unpkg.com/[email protected]/dist/ unpkg.com/[email protected]/; style-src 'httputils-nonce' unpkg.com/[email protected]/dist/ unpkg.com/[email protected]/; img-src 'self' data: a.tile.openstreetmap.org b.tile.openstreetmap.org c.tile.openstreetmap.org"))
owaspConfig := owasp.Flags(fs, "", flags.NewOverride("FrameOptions", "SAMEORIGIN"), flags.NewOverride("Csp", "default-src 'self'; base-uri 'self'; script-src 'self' 'httputils-nonce' unpkg.com/[email protected]/dist/ unpkg.com/[email protected]/; style-src 'httputils-nonce' unpkg.com/[email protected]/dist/ unpkg.com/[email protected]/; img-src 'self' data: a.tile.openstreetmap.org b.tile.openstreetmap.org c.tile.openstreetmap.org"))

basicConfig := basicMemory.Flags(fs, "auth", flags.NewOverride("Profiles", "1:admin"))

Expand Down
122 changes: 122 additions & 0 deletions cmd/fibr/static/scripts/thumbnail.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// from https://developers.google.com/speed/webp/faq#how_can_i_detect_browser_support_for_webp
async function isWebPCompatible() {
const animatedImage =
'UklGRlIAAABXRUJQVlA4WAoAAAASAAAAAAAAAAAAQU5JTQYAAAD/////AABBTk1GJgAAAAAAAAAAAAAAAAAAAGQAAABWUDhMDQAAAC8AAAAQBxAREYiI/gcA';

return new Promise((resolve, reject) => {
var image = new Image();
image.onload = () => {
if (image.width > 0 && image.height > 0) {
resolve();
} else {
reject();
}
};

image.onerror = reject;
image.src = `data:image/webp;base64,${animatedImage}`;
});
}

// From https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultReader/read#example_2_-_handling_text_line_by_line
async function* readLineByLine(response) {
const utf8Decoder = new TextDecoder('utf-8');
const reader = response.body.getReader();
let { value: chunk, done: readerDone } = await reader.read();
chunk = chunk ? utf8Decoder.decode(chunk, { stream: true }) : '';

let re = /\r\n|\n|\r/gm;
let startIndex = 0;

for (;;) {
const result = re.exec(chunk);
if (!result) {
if (readerDone) {
break;
}

const remainder = chunk.substr(startIndex);
({ value: chunk, done: readerDone } = await reader.read());
chunk =
remainder + (chunk ? utf8Decoder.decode(chunk, { stream: true }) : '');
startIndex = re.lastIndex = 0;
continue;
}

yield chunk.substring(startIndex, result.index);
startIndex = re.lastIndex;
}

if (startIndex < chunk.length) {
yield chunk.substr(startIndex);
}
}

/**
* Async image loading
*/
async function fetchThumbnail() {
fetchURL = document.location.search;
if (fetchURL.includes('?')) {
fetchURL += '&thumbnail';
} else {
fetchURL += '?thumbnail';
}

const response = await fetch(fetchURL, { credentials: 'same-origin' });

if (response.status >= 400) {
throw new Error('unable to load thumbnails');
}

for await (let line of readLineByLine(response)) {
const parts = line.split(',');
if (parts.length != 2) {
console.error('invalid line for thumbnail:', line);
continue;
}

const picture = document.getElementById(`picture-${parts[0]}`);
if (!picture) {
continue;
}

const img = new Image();
img.src = `data:image/webp;base64,${parts[1]}`;
img.alt = picture.dataset.alt;
img.dataset.src = picture.dataset.src;
img.classList.add('thumbnail', 'full', 'block');

replaceContent(picture, img);
}
}

window.addEventListener(
'load',
async () => {
const thumbnailsElem = document.querySelectorAll('[data-thumbnail]');
if (!thumbnailsElem) {
return;
}

try {
await isWebPCompatible();
} catch (e) {
console.error('Your browser is not compatible with WebP format.', e);
thumbnailsElem.forEach(displayNoThumbnail);
return;
}

thumbnailsElem.forEach((picture) => {
replaceContent(picture, generateThrobber(['throbber-white']));
});

try {
await fetchThumbnail();
window.dispatchEvent(new Event('thumbnail-done'));
} catch (e) {
console.error(e);
}
},
false,
);
1 change: 1 addition & 0 deletions cmd/fibr/static/scripts/thumbnail/web.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions cmd/fibr/static/scripts/thumbnail/web.js.map

Large diffs are not rendered by default.

122 changes: 2 additions & 120 deletions cmd/fibr/templates/async-image.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,25 +24,6 @@
</style>

<script type="text/javascript" nonce="{{ .nonce }}">
// from https://developers.google.com/speed/webp/faq#how_can_i_detect_browser_support_for_webp
async function isWebPCompatible() {
const animatedImage = 'UklGRlIAAABXRUJQVlA4WAoAAAASAAAAAAAAAAAAQU5JTQYAAAD/////AABBTk1GJgAAAAAAAAAAAAAAAAAAAGQAAABWUDhMDQAAAC8AAAAQBxAREYiI/gcA';

return new Promise((resolve, reject) => {
var image = new Image();
image.onload = () => {
if (image.width > 0 && image.height > 0) {
resolve();
} else {
reject();
}
};

image.onerror = reject;
image.src = `data:image/webp;base64,${animatedImage}`;
});
}

/**
* Restore default layout when no thumbnail is possible.
*/
Expand All @@ -56,107 +37,8 @@
picture.classList.add('icon', 'icon-large');
picture.alt = 'Image file';
}
</script>

// From https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultReader/read#example_2_-_handling_text_line_by_line
async function* readLineByLine(response) {
const utf8Decoder = new TextDecoder('utf-8');
const reader = response.body.getReader();
let { value: chunk, done: readerDone } = await reader.read();
chunk = chunk ? utf8Decoder.decode(chunk, { stream: true }) : '';

let re = /\r\n|\n|\r/gm;
let startIndex = 0;

for (;;) {
const result = re.exec(chunk);
if (!result) {
if (readerDone) {
break;
}

const remainder = chunk.substr(startIndex);
({ value: chunk, done: readerDone } = await reader.read());
chunk = remainder + (chunk ? utf8Decoder.decode(chunk, { stream: true }) : '');
startIndex = re.lastIndex = 0;
continue;
}

yield chunk.substring(startIndex, result.index);
startIndex = re.lastIndex;
}

if (startIndex < chunk.length) {
yield chunk.substr(startIndex);
}
}

/**
* Async image loading
*/
async function fetchThumbnail() {
fetchURL = document.location.search
if (fetchURL.includes('?')) {
fetchURL += "&thumbnail"
} else {
fetchURL += "?thumbnail"
}

const response = await fetch(fetchURL, { credentials: 'same-origin' });

if (response.status >= 400) {
throw new Error('unable to load thumbnails');
}

for await (let line of readLineByLine(response)) {
const parts = line.split(',');
if (parts.length != 2) {
console.error('invalid line for thumbnail:', line);
continue;
}

const picture = document.getElementById(`picture-${parts[0]}`);
if (!picture) {
continue;
}

const img = new Image();
img.src = `data:image/webp;base64,${parts[1]}`;
img.alt = picture.dataset.alt;
img.dataset.src = picture.dataset.src;
img.classList.add('thumbnail', 'full', 'block');

replaceContent(picture, img);
}
}

window.addEventListener(
'load',
async () => {
const thumbnailsElem = document.querySelectorAll('[data-thumbnail]');
if (!thumbnailsElem) {
return;
}

try {
await isWebPCompatible();
} catch (e) {
console.error('Your browser is not compatible with WebP format.', e);
thumbnailsElem.forEach(displayNoThumbnail);
return;
}

thumbnailsElem.forEach((picture) => {
replaceContent(picture, generateThrobber(['throbber-white']));
});

try {
await fetchThumbnail();
window.dispatchEvent(new Event('thumbnail-done'))
} catch (e) {
console.error(e);
}
},
false,
);
<script type="text/javascript" src="/scripts/thumbnail/web.js" async></script>
</script>
{{ end }}
20 changes: 20 additions & 0 deletions pkg/crud/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ func (a App) handleMultipart(w http.ResponseWriter, r *http.Request, request pro
}

func (a App) handlePostShare(w http.ResponseWriter, r *http.Request, request provider.Request, method string) {
if !request.CanShare {
a.error(w, r, request, model.WrapForbidden(ErrNotAuthorized))
return
}

switch method {
case http.MethodPost:
a.createShare(w, r, request)
Expand All @@ -128,6 +133,11 @@ func (a App) handlePostShare(w http.ResponseWriter, r *http.Request, request pro
}

func (a App) handlePostWebhook(w http.ResponseWriter, r *http.Request, request provider.Request, method string) {
if !request.CanWebhook {
a.error(w, r, request, model.WrapForbidden(ErrNotAuthorized))
return
}

switch method {
case http.MethodPost:
a.createWebhook(w, r, request)
Expand All @@ -139,6 +149,11 @@ func (a App) handlePostWebhook(w http.ResponseWriter, r *http.Request, request p
}

func (a App) handlePostDescription(w http.ResponseWriter, r *http.Request, request provider.Request, method string) {
if !request.CanEdit {
a.error(w, r, request, model.WrapForbidden(ErrNotAuthorized))
return
}

name, err := checkFormName(r, "name")
if err != nil && !errors.Is(err, ErrEmptyName) {
a.error(w, r, request, err)
Expand Down Expand Up @@ -172,6 +187,11 @@ func (a App) handlePostDescription(w http.ResponseWriter, r *http.Request, reque
}

func (a App) handlePost(w http.ResponseWriter, r *http.Request, request provider.Request, method string) {
if !request.CanEdit {
a.error(w, r, request, model.WrapForbidden(ErrNotAuthorized))
return
}

switch method {
case http.MethodPatch:
a.Rename(w, r, request)
Expand Down
5 changes: 0 additions & 5 deletions pkg/crud/regenerate.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,6 @@ import (
)

func (a App) regenerate(w http.ResponseWriter, r *http.Request, request provider.Request) {
if !request.CanEdit {
a.error(w, r, request, model.WrapForbidden(ErrNotAuthorized))
return
}

pathname := request.Filepath()
ctx := r.Context()

Expand Down
10 changes: 0 additions & 10 deletions pkg/crud/share.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,6 @@ func parseRights(value string) (edit, story bool, err error) {
}

func (a App) createShare(w http.ResponseWriter, r *http.Request, request provider.Request) {
if !request.CanShare {
a.error(w, r, request, model.WrapForbidden(ErrNotAuthorized))
return
}

var err error

edit, story, err := parseRights(strings.TrimSpace(r.FormValue("rights")))
Expand Down Expand Up @@ -127,11 +122,6 @@ func (a App) createShare(w http.ResponseWriter, r *http.Request, request provide
}

func (a App) deleteShare(w http.ResponseWriter, r *http.Request, request provider.Request) {
if !request.CanShare {
a.error(w, r, request, model.WrapForbidden(ErrNotAuthorized))
return
}

id := r.FormValue("id")

if err := a.shareApp.Delete(r.Context(), id); err != nil {
Expand Down
Loading

0 comments on commit 5ac65ec

Please sign in to comment.