Pull to refresh
232.44
ITSumma
Эксперты в производительности

Хостинг сайта на Imgur

Reading time6 min
Views2.8K


В интернете трудно найти нормальный хостинг для файлов, зато есть огромное количество бесплатных хостингов картинок вроде 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, который позволяет разработчикам создавать новые типы файловых систем, доступные для монтирования пользователями без привилегий.
Tags:
Hubs:
Total votes 19: ↑19 and ↓0+19
Comments2

Other news

Information

Website
www.itsumma.ru
Registered
Founded
Employees
101–200 employees
Location
Россия
Representative
ITSumma