Free stroage if it is exceeding disk quota (#7061)
This commit is contained in:
		
				
					committed by
					
						
						Eugen Rochko
					
				
			
			
				
	
			
			
			
						parent
						
							b83ce18b30
						
					
				
				
					commit
					1ed1014546
				
			@@ -1,5 +1,5 @@
 | 
			
		||||
import api, { getLinks } from '../api';
 | 
			
		||||
import asyncDB from '../storage/db';
 | 
			
		||||
import openDB from '../storage/db';
 | 
			
		||||
import { importAccount, importFetchedAccount, importFetchedAccounts } from './importer';
 | 
			
		||||
 | 
			
		||||
export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
 | 
			
		||||
@@ -94,12 +94,15 @@ export function fetchAccount(id) {
 | 
			
		||||
 | 
			
		||||
    dispatch(fetchAccountRequest(id));
 | 
			
		||||
 | 
			
		||||
    asyncDB.then(db => getFromDB(
 | 
			
		||||
    openDB().then(db => getFromDB(
 | 
			
		||||
      dispatch,
 | 
			
		||||
      getState,
 | 
			
		||||
      db.transaction('accounts', 'read').objectStore('accounts').index('id'),
 | 
			
		||||
      id
 | 
			
		||||
    )).catch(() => api(getState).get(`/api/v1/accounts/${id}`).then(response => {
 | 
			
		||||
    ).then(() => db.close(), error => {
 | 
			
		||||
      db.close();
 | 
			
		||||
      throw error;
 | 
			
		||||
    })).catch(() => api(getState).get(`/api/v1/accounts/${id}`).then(response => {
 | 
			
		||||
      dispatch(importFetchedAccount(response.data));
 | 
			
		||||
    })).then(() => {
 | 
			
		||||
      dispatch(fetchAccountSuccess());
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
import { autoPlayGif } from '../../initial_state';
 | 
			
		||||
import { putAccounts, putStatuses } from '../../storage/modifier';
 | 
			
		||||
import { normalizeAccount, normalizeStatus } from './normalizer';
 | 
			
		||||
 | 
			
		||||
@@ -44,7 +45,7 @@ export function importFetchedAccounts(accounts) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  accounts.forEach(processAccount);
 | 
			
		||||
  putAccounts(normalAccounts);
 | 
			
		||||
  putAccounts(normalAccounts, !autoPlayGif);
 | 
			
		||||
 | 
			
		||||
  return importAccounts(normalAccounts);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import api from '../api';
 | 
			
		||||
import asyncDB from '../storage/db';
 | 
			
		||||
import openDB from '../storage/db';
 | 
			
		||||
import { evictStatus } from '../storage/modifier';
 | 
			
		||||
 | 
			
		||||
import { deleteFromTimelines } from './timelines';
 | 
			
		||||
@@ -92,12 +92,17 @@ export function fetchStatus(id) {
 | 
			
		||||
 | 
			
		||||
    dispatch(fetchStatusRequest(id, skipLoading));
 | 
			
		||||
 | 
			
		||||
    asyncDB.then(db => {
 | 
			
		||||
    openDB().then(db => {
 | 
			
		||||
      const transaction = db.transaction(['accounts', 'statuses'], 'read');
 | 
			
		||||
      const accountIndex = transaction.objectStore('accounts').index('id');
 | 
			
		||||
      const index = transaction.objectStore('statuses').index('id');
 | 
			
		||||
 | 
			
		||||
      return getFromDB(dispatch, getState, accountIndex, index, id);
 | 
			
		||||
      return getFromDB(dispatch, getState, accountIndex, index, id).then(() => {
 | 
			
		||||
        db.close();
 | 
			
		||||
      }, error => {
 | 
			
		||||
        db.close();
 | 
			
		||||
        throw error;
 | 
			
		||||
      });
 | 
			
		||||
    }).then(() => {
 | 
			
		||||
      dispatch(fetchStatusSuccess(skipLoading));
 | 
			
		||||
    }, () => api(getState).get(`/api/v1/statuses/${id}`).then(response => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
import { freeStorage } from '../storage/modifier';
 | 
			
		||||
import './web_push_notifications';
 | 
			
		||||
 | 
			
		||||
function openSystemCache() {
 | 
			
		||||
@@ -42,8 +43,10 @@ self.addEventListener('fetch', function(event) {
 | 
			
		||||
 | 
			
		||||
    event.respondWith(asyncResponse.then(async response => {
 | 
			
		||||
      if (response.ok || response.type === 'opaqueredirect') {
 | 
			
		||||
        const cache = await asyncCache;
 | 
			
		||||
        await cache.delete('/');
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
          asyncCache.then(cache => cache.delete('/')),
 | 
			
		||||
          indexedDB.deleteDatabase('mastodon'),
 | 
			
		||||
        ]);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return response;
 | 
			
		||||
@@ -56,7 +59,11 @@ self.addEventListener('fetch', function(event) {
 | 
			
		||||
        const fetched = await fetch(event.request);
 | 
			
		||||
 | 
			
		||||
        if (fetched.ok) {
 | 
			
		||||
          await cache.put(event.request.url, fetched.clone());
 | 
			
		||||
          try {
 | 
			
		||||
            await cache.put(event.request.url, fetched.clone());
 | 
			
		||||
          } finally {
 | 
			
		||||
            freeStorage();
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return fetched;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,14 @@
 | 
			
		||||
import { me } from '../initial_state';
 | 
			
		||||
 | 
			
		||||
export default new Promise((resolve, reject) => {
 | 
			
		||||
export default () => new Promise((resolve, reject) => {
 | 
			
		||||
  // ServiceWorker is required to synchronize the login state.
 | 
			
		||||
  // Microsoft Edge 17 does not support getAll according to:
 | 
			
		||||
  // Catalog of standard and vendor APIs across browsers - Microsoft Edge Development
 | 
			
		||||
  // https://developer.microsoft.com/en-us/microsoft-edge/platform/catalog/?q=specName%3Aindexeddb
 | 
			
		||||
  if (!me || !('getAll' in IDBObjectStore.prototype)) {
 | 
			
		||||
  if (!('caches' in self && 'getAll' in IDBObjectStore.prototype)) {
 | 
			
		||||
    reject();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const request = indexedDB.open('mastodon:' + me);
 | 
			
		||||
  const request = indexedDB.open('mastodon');
 | 
			
		||||
 | 
			
		||||
  request.onerror = reject;
 | 
			
		||||
  request.onsuccess = ({ target }) => resolve(target.result);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,14 @@
 | 
			
		||||
import asyncDB from './db';
 | 
			
		||||
import { autoPlayGif } from '../initial_state';
 | 
			
		||||
import openDB from './db';
 | 
			
		||||
 | 
			
		||||
const accountAssetKeys = ['avatar', 'avatar_static', 'header', 'header_static'];
 | 
			
		||||
const avatarKey = autoPlayGif ? 'avatar' : 'avatar_static';
 | 
			
		||||
const limit = 1024;
 | 
			
		||||
const storageMargin = 8388608;
 | 
			
		||||
const storeLimit = 1024;
 | 
			
		||||
 | 
			
		||||
// ServiceWorker and Cache API is not available on iOS 11
 | 
			
		||||
// https://webkit.org/status/#specification-service-workers
 | 
			
		||||
const asyncCache = window.caches ? caches.open('mastodon-system') : Promise.reject();
 | 
			
		||||
function openCache() {
 | 
			
		||||
  // ServiceWorker and Cache API is not available on iOS 11
 | 
			
		||||
  // https://webkit.org/status/#specification-service-workers
 | 
			
		||||
  return self.caches ? caches.open('mastodon-system') : Promise.reject();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function printErrorIfAvailable(error) {
 | 
			
		||||
  if (error) {
 | 
			
		||||
@@ -16,7 +17,7 @@ function printErrorIfAvailable(error) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function put(name, objects, onupdate, oncreate) {
 | 
			
		||||
  return asyncDB.then(db => new Promise((resolve, reject) => {
 | 
			
		||||
  return openDB().then(db => (new Promise((resolve, reject) => {
 | 
			
		||||
    const putTransaction = db.transaction(name, 'readwrite');
 | 
			
		||||
    const putStore = putTransaction.objectStore(name);
 | 
			
		||||
    const putIndex = putStore.index('id');
 | 
			
		||||
@@ -53,7 +54,7 @@ function put(name, objects, onupdate, oncreate) {
 | 
			
		||||
      const count = readStore.count();
 | 
			
		||||
 | 
			
		||||
      count.onsuccess = () => {
 | 
			
		||||
        const excess = count.result - limit;
 | 
			
		||||
        const excess = count.result - storeLimit;
 | 
			
		||||
 | 
			
		||||
        if (excess > 0) {
 | 
			
		||||
          const retrieval = readStore.getAll(null, excess);
 | 
			
		||||
@@ -69,11 +70,17 @@ function put(name, objects, onupdate, oncreate) {
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    putTransaction.onerror = reject;
 | 
			
		||||
  })).then(resolved => {
 | 
			
		||||
    db.close();
 | 
			
		||||
    return resolved;
 | 
			
		||||
  }, error => {
 | 
			
		||||
    db.close();
 | 
			
		||||
    throw error;
 | 
			
		||||
  }));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function evictAccountsByRecords(records) {
 | 
			
		||||
  asyncDB.then(db => {
 | 
			
		||||
  return openDB().then(db => {
 | 
			
		||||
    const transaction = db.transaction(['accounts', 'statuses'], 'readwrite');
 | 
			
		||||
    const accounts = transaction.objectStore('accounts');
 | 
			
		||||
    const accountsIdIndex = accounts.index('id');
 | 
			
		||||
@@ -83,7 +90,7 @@ function evictAccountsByRecords(records) {
 | 
			
		||||
 | 
			
		||||
    function evict(toEvict) {
 | 
			
		||||
      toEvict.forEach(record => {
 | 
			
		||||
        asyncCache
 | 
			
		||||
        openCache()
 | 
			
		||||
          .then(cache => accountAssetKeys.forEach(key => cache.delete(records[key])))
 | 
			
		||||
          .catch(printErrorIfAvailable);
 | 
			
		||||
 | 
			
		||||
@@ -98,6 +105,8 @@ function evictAccountsByRecords(records) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    evict(records);
 | 
			
		||||
 | 
			
		||||
    db.close();
 | 
			
		||||
  }).catch(printErrorIfAvailable);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -106,8 +115,9 @@ export function evictStatus(id) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function evictStatuses(ids) {
 | 
			
		||||
  asyncDB.then(db => {
 | 
			
		||||
    const store = db.transaction('statuses', 'readwrite').objectStore('statuses');
 | 
			
		||||
  return openDB().then(db => {
 | 
			
		||||
    const transaction = db.transaction('statuses', 'readwrite');
 | 
			
		||||
    const store = transaction.objectStore('statuses');
 | 
			
		||||
    const idIndex = store.index('id');
 | 
			
		||||
    const reblogIndex = store.index('reblog');
 | 
			
		||||
 | 
			
		||||
@@ -118,14 +128,17 @@ export function evictStatuses(ids) {
 | 
			
		||||
      idIndex.getKey(id).onsuccess =
 | 
			
		||||
        ({ target }) => target.result && store.delete(target.result);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    db.close();
 | 
			
		||||
  }).catch(printErrorIfAvailable);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function evictStatusesByRecords(records) {
 | 
			
		||||
  evictStatuses(records.map(({ id }) => id));
 | 
			
		||||
  return evictStatuses(records.map(({ id }) => id));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function putAccounts(records) {
 | 
			
		||||
export function putAccounts(records, avatarStatic) {
 | 
			
		||||
  const avatarKey = avatarStatic ? 'avatar_static' : 'avatar';
 | 
			
		||||
  const newURLs = [];
 | 
			
		||||
 | 
			
		||||
  put('accounts', records, (newRecord, oldKey, store, oncomplete) => {
 | 
			
		||||
@@ -135,7 +148,7 @@ export function putAccounts(records) {
 | 
			
		||||
        const oldURL = target.result[key];
 | 
			
		||||
 | 
			
		||||
        if (newURL !== oldURL) {
 | 
			
		||||
          asyncCache
 | 
			
		||||
          openCache()
 | 
			
		||||
            .then(cache => cache.delete(oldURL))
 | 
			
		||||
            .catch(printErrorIfAvailable);
 | 
			
		||||
        }
 | 
			
		||||
@@ -153,11 +166,12 @@ export function putAccounts(records) {
 | 
			
		||||
  }, (newRecord, oncomplete) => {
 | 
			
		||||
    newURLs.push(newRecord[avatarKey]);
 | 
			
		||||
    oncomplete();
 | 
			
		||||
  }).then(records => {
 | 
			
		||||
    evictAccountsByRecords(records);
 | 
			
		||||
    asyncCache
 | 
			
		||||
      .then(cache => cache.addAll(newURLs))
 | 
			
		||||
      .catch(printErrorIfAvailable);
 | 
			
		||||
  }).then(records => Promise.all([
 | 
			
		||||
    evictAccountsByRecords(records),
 | 
			
		||||
    openCache().then(cache => cache.addAll(newURLs)),
 | 
			
		||||
  ])).then(freeStorage, error => {
 | 
			
		||||
    freeStorage();
 | 
			
		||||
    throw error;
 | 
			
		||||
  }).catch(printErrorIfAvailable);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -166,3 +180,27 @@ export function putStatuses(records) {
 | 
			
		||||
    .then(evictStatusesByRecords)
 | 
			
		||||
    .catch(printErrorIfAvailable);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function freeStorage() {
 | 
			
		||||
  return navigator.storage.estimate().then(({ quota, usage }) => {
 | 
			
		||||
    if (usage + storageMargin < quota) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return openDB().then(db => new Promise((resolve, reject) => {
 | 
			
		||||
      const retrieval = db.transaction('accounts', 'readonly').objectStore('accounts').getAll(null, 1);
 | 
			
		||||
 | 
			
		||||
      retrieval.onsuccess = () => {
 | 
			
		||||
        if (retrieval.result.length > 0) {
 | 
			
		||||
          resolve(evictAccountsByRecords(retrieval.result).then(freeStorage));
 | 
			
		||||
        } else {
 | 
			
		||||
          resolve(caches.delete('mastodon-system'));
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      retrieval.onerror = reject;
 | 
			
		||||
 | 
			
		||||
      db.close();
 | 
			
		||||
    }));
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user