В интернете трудно найти нормальный хостинг для файлов, зато есть огромное количество бесплатных хостингов картинок вроде Imgur или Flickr. Поэтому давным-давно появилась идея размещать там произвольные файлы под видом картинок (есть масса плагинов, чтобы заливать на Flickr любые файлы или прятать произвольные файлы внутри настоящих фотографий). Сейчас эта концепция продвинулась ещё дальше.
Если вкратце, то экспериментальный инструмент Web2img сначала перекодирует файлы вашего веб-сайта в формат изображений (для размещения на хостинге), а затем преобразует эту картинку в JS-скрипт для выполнения в браузере на лету (через service worker). Таким образом, контент сайта загружается с Imgur прямо в браузер.
На демо-странице можно посмотреть, как выглядит результат. Например, вот исходные файлы тестового сайта (11 файлов, 1 738 978 байт). С помощью Web2img они преобразуются в картинку
6e69835a-3680-4c73-9940-d733464bddc3.png
размером 762х762 пикселя.Сохраняем её на Imgur:
https://i.imgur.com/mGU3YV1.png
.В демо-инструменте эта картинка преобразуется в скрипт x.js для запуска в браузере. Его можно минифицировать или не минифицировать. Вот код скрипта в обычном виде:
Код скрипта
var HASH = 'dXZrNL4q6Lr6KzN0ECHIW4RP8hQy9uL2W9uBP/CJ/4Y='
var URLS = ['https://i.imgur.com/mGU3YV1.png']
var PRIVACY = 2
var UPDATE_INTERVAL = 120
var IMG_TIMEOUT = 10
function pageEnv() {
var container = document.documentElement
function fallback(html) {
var noscripts = document.getElementsByTagName('noscript')
if (noscripts.length > 0) {
html = noscripts[0].innerHTML
}
container.innerHTML = html
}
var jsUrl = document.currentScript.src
var sw = navigator.serviceWorker
if (!sw) {
fallback('Service Worker is not supported')
return
}
var rootPath = getRootPath(jsUrl)
function unpackToCache(bytes, cache) {
var pendings = []
if (!sw.controller) {
var swPending = sw.register(jsUrl).catch(function(err) {
fallback(err.message)
})
pendings.push(swPending)
}
var info = JSON.stringify({
hash: HASH,
time: Date.now()
})
var res = new Response(info)
pendings.push([
cache.put(rootPath + '.cache-info', res),
])
var pathResMap = unpack(bytes)
for (var path in pathResMap) {
res = pathResMap[path]
pendings.push(
cache.put(rootPath + path, res)
)
}
Promise.all(pendings).then(function() {
location.reload()
})
}
function parseImgBuf(buf) {
if (!buf) {
loadNextUrl()
return
}
crypto.subtle.digest('SHA-256', buf).then(function(digest) {
var hashBin = new Uint8Array(digest)
var hashB64 = btoa(String.fromCharCode.apply(null, hashBin))
if (HASH && HASH !== hashB64) {
console.warn('[web2img] bad hash. exp:', HASH, 'but got:', hashB64)
loadNextUrl()
return
}
var bytes = decode1Px3Bytes(buf)
caches.delete('.web2img').then(function() {
caches.open('.web2img').then(function(cache) {
unpackToCache(bytes, cache)
})
})
})
}
// run in iframe
var loadImg = function(e) {
var opt = e.data
var img = new Image()
img.onload = function() {
clearInterval(tid)
var canvas = document.createElement('canvas')
canvas.width = img.width
canvas.height = img.height
var ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0)
var imgData = ctx.getImageData(0, 0, img.width, img.height)
var buf = imgData.data.buffer
if (opt.privacy === 2) {
parent.postMessage(buf, '*', [buf])
} else {
parseImgBuf(buf)
}
}
img.onerror = function() {
clearInterval(tid)
if (opt.privacy === 2) {
parent.postMessage('', '*')
} else {
parseImgBuf()
}
}
if (opt.privacy === 1) {
img.referrerPolicy = 'no-referrer'
}
img.crossOrigin = 1
img.src = opt.url
var tid = setTimeout(function() {
console.log('[web2img] timeout:', opt.url)
img.onerror()
img.onerror = img.onload = null
img.src = ''
}, opt.timeout)
}
if (PRIVACY === 2) {
// hide `origin` header
var iframe = document.createElement('iframe')
if (typeof RELEASE !== 'undefined') {
iframe.src = 'data:text/html,<script>onmessage=' + loadImg + '</script>'
} else {
iframe.src = 'data:text/html;base64,' + btoa('<script>onmessage=' + loadImg + '</script>')
}
iframe.style.display = 'none'
iframe.onload = loadNextUrl
container.appendChild(iframe)
var iframeWin = iframe.contentWindow
self.onmessage = function(e) {
if (e.source === iframeWin) {
parseImgBuf(e.data)
}
}
} else {
loadNextUrl()
}
function loadNextUrl() {
var url = URLS.shift()
if (!url) {
fallback('failed to load resources')
return
}
var opt = {
url: url,
privacy: PRIVACY,
timeout: IMG_TIMEOUT * 1000
}
if (PRIVACY === 2) {
iframeWin.postMessage(opt, '*')
} else {
loadImg({data: opt})
}
}
function decode1Px3Bytes(pixelBuf) {
var u32 = new Uint32Array(pixelBuf)
var out = new Uint8Array(u32.length * 3)
var p = 0
u32.forEach(function(rgba) {
out[p++] = rgba
out[p++] = rgba >> 8
out[p++] = rgba >> 16
})
return out
}
function unpack(bytes) {
var confEnd = bytes.indexOf(13) // '\r'
var confBin = bytes.subarray(0, confEnd)
var confStr = new TextDecoder().decode(confBin)
var confObj = JSON.parse(confStr)
var offset = confEnd + 1
for (var path in confObj) {
var headers = confObj[path]
var expires = /\.html$/.test(path) ? 5 : UPDATE_INTERVAL
headers['cache-control'] = 'max-age=' + expires
var len = +headers['content-length']
var bin = bytes.subarray(offset, offset + len)
confObj[path] = new Response(bin, {
headers: headers
})
offset += len
}
return confObj
}
}
function swEnv() {
var jsUrl = location.href.split('?')[0]
var rootPath = getRootPath(jsUrl)
var isFirst = 1
var newJs
function openFile(path) {
return caches.open('.web2img').then(function(cache) {
return cache.match(path)
})
}
function checkUpdate() {
openFile(rootPath + '.cache-info').then(function(res) {
if (!res) {
return
}
res.json().then(function(info) {
if (Date.now() - info.time < 1000 * UPDATE_INTERVAL) {
return
}
var url, opt
if ('cache' in Request.prototype) {
url = jsUrl
opt = {cache: 'no-cache'}
} else {
url = jsUrl + '?t=' + Date.now()
}
fetch(url, opt).then(function(res) {
res.text().then(function(js) {
if (js.indexOf(info.hash) === -1) {
newJs = url
console.log('[web2img] new version found')
}
})
})
})
})
}
setInterval(checkUpdate, 1000 * UPDATE_INTERVAL)
function respondFile(url) {
var path = new URL(url).pathname
.replace(/\/{2,}/g, '/')
.replace(/\/$/, '/index.html')
return openFile(path).then(function(r1) {
return r1 || openFile(rootPath + '404.html').then(function(r2) {
return r2 || new Response('file not found: ' + path, {
status: 404
})
})
})
}
function respond(req) {
return caches.has('.web2img').then(function(existed) {
if (!existed) {
// fix cache
newJs = jsUrl
}
if (newJs && req.mode === 'navigate') {
var res = new Response('<script src=' + newJs + '></script>', {
headers: {
'content-type': 'text/html'
}
})
newJs = ''
console.log('[web2img] updating')
return res
}
return respondFile(req.url)
})
}
onfetch = function(e) {
if (isFirst) {
isFirst = 0
checkUpdate()
}
var req = e.request
if (req.url.indexOf(rootPath) === 0 && req.url.indexOf(jsUrl) !== 0) {
// url starts with rootPath (exclude x.js)
e.respondWith(respond(req))
}
}
oninstall = function() {
skipWaiting()
}
}
function getRootPath(url) {
// e.g.
// 'https://mysite.com/'
// 'https://xx.github.io/path/to/'
return url.split('?')[0].replace(/[^/]+$/, '')
}
if (self.document) {
pageEnv()
} else {
swEnv()
}
Осталось только запустить скрипт в браузере. Например, со странички
404.html
:<script src=/x.js></script>
Таким образом, Imgur выполняет роль бесплатного CDN или бесплатного хостинга для загрузки файлов вашего сайта в браузер пользователя.
В принципе, это не новая идея. Наличие бесплатных хостингов картинок само собой наталкивает на мысль перекодировать файлы любого формата в картинки и использовать этот хостинг в качестве хранилища для файлов любого типа. Например, плагин PngEncoder для Flickr позволял гибридному облачному сервису Syncany использовать в бэкенде хостинг фотографий Flickr, который бесплатно принимает до 1 ТБ файлов. С аналогичными целями можно использовать Google Photos и другие бесплатные хостинги фотографий, хотя подобный конвертация будет нарушать официальные правила использования этих сервисов (ToS).
Ещё одна экспериментальная программа flickr-music-player конвертирует музыкальные файлы в картинки — и размещает на Flickr, пример (музыкальные файлы даже рендерятся как настоящие картинки, в соответствии с обложкой своего музыкального альбома).
Можно вспомнить также кодировщики Flickr-FS и flickr-store для быстрого перекодирования файлов в картинки PNG, которые принимает Flickr.
В определённом смысле, такие интерфейсы превращают хостинги картинок в полноценные файловые системы, словно модуль FUSE под Linux, который позволяет разработчикам создавать новые типы файловых систем, доступные для монтирования пользователями без привилегий.